# LearnSystemC AI Context Bundle

Use this Markdown file as retrieval context for an AI assistant, coding agent, or notebook when working on SystemC models.
It contains the LearnSystemC teaching corpus in curriculum order. It is educational context, not a replacement for the official LRMs.
For version-sensitive behavior, verify the relevant LRM and official Accellera source repository before changing production code.

## Recommended AI Instructions

When answering SystemC questions:

1. Separate portable LRM behavior from Accellera implementation detail.
2. Name the relevant SystemC family and version baseline.
3. Explain the mental model before presenting syntax.
4. Prefer compact runnable C++ examples.
5. Call out scheduler phases, delta cycles, elaboration timing, binding rules, and TLM timing assumptions when relevant.
6. Treat source paths as implementation trails, not portable API contracts.

## Version Baselines

- SystemC core and TLM: SystemC 3.0.2 with IEEE 1666-2023.
- SystemC AMS: SystemC AMS 2.0 LRM.
- SystemC CCI: CCI 1.0 LRM and CCI 1.0.1 proof-of-concept source.
- UVM-SystemC: UVM-SystemC 1.0-beta6 public-review signal; the local February 2023 draft LRM remains the clause-reading baseline.
- SystemC Synthesis Subset: SystemC Synthesis Subset 1.4.7 LRM.

## Official Source Repositories

- https://github.com/accellera-official/systemc
- https://github.com/accellera-official/cci
- https://github.com/accellera-official/uvm-systemc
- https://github.com/accellera-official/systemc-common-practices

## Useful Public Files

- Compact Markdown cheat sheet: https://www.learn-systemc.com/systemc-cheat-sheet.md
- AI crawler index: https://www.learn-systemc.com/llms.txt
- Sitemap: https://www.learn-systemc.com/sitemap.xml

## Curriculum Corpus (137 lessons)


## Lesson 1: C++ Prerequisites

Canonical lesson: https://www.learn-systemc.com/tutorials/001-cpp-prerequisites

What you absolutely must know about C++ before attempting to learn SystemC.

# C++ Prerequisites for SystemC

## How to Read This Lesson

Read this as the C++ toolkit you will keep reusing throughout the course. SystemC feels much less mysterious once you can see the ordinary C++ mechanisms underneath its hardware-modeling vocabulary.

Before diving into SystemC, it is crucial to understand that **SystemC is not a new language**. It is entirely built as a C++ class library. If you are coming from Verilog or VHDL, you might be tempted to treat SystemC like a hardware description language. However, if you do not understand the underlying C++, you will quickly become frustrated when the compiler throws massive template errors.

This page outlines exactly what C++ concepts you *must* master before proceeding.

> [!NOTE]
> **Disclaimer:** The SystemC code shown in this chapter is strictly to demonstrate how C++ concepts are leveraged. We will dive deep into how these SystemC components actually work in their respective chapters.

## Standard and source context

For this foundation lesson, keep three references close: `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for portable semantics, `Accellera SystemC GitHub repository` for kernel behavior, and `Accellera SystemC GitHub repository` for bit-accurate C++ types. When this lesson mentions a macro or type, the useful habit is to ask which C++ class the macro eventually creates.

## 1. Object-Oriented Programming (OOP)
SystemC modules (`sc_module`) are just C++ classes. You need to be deeply comfortable with:
- **Classes and Structs:** Creating them, member variables, and member functions.
- **Constructors:** SystemC relies heavily on constructors (`SC_CTOR`) to register processes and bind ports.
- **Inheritance:** `sc_module` inherits from `sc_object`. You will frequently use public inheritance when defining custom interfaces.
- **Polymorphism and Virtual Functions:** In TLM (Transaction Level Modeling), you will define purely virtual interfaces and implement them in your target modules.

## 2. Pointers and References
Hardware ports are essentially safe pointers to signals.
- **Pass-by-Reference (`&`):** Passing large transaction payloads efficiently.
- **Pointers (`*`):** Dynamically allocating modules, or passing pointers to arrays.
- **Smart Pointers:** In advanced TLM and SCV (SystemC Verification Library), you will use `scv_smart_ptr` to handle memory automatically and avoid segmentation faults.

## 3. C++ Templates
SystemC ports, signals, and FIFOs are **templated classes**.
When you write `sc_in<bool>`, you are instantiating an `sc_in` template for the boolean type.

You must understand:
- How to pass basic types to templates (`sc_signal<int>`).
- How to pass user-defined structs to templates (`sc_signal<MyStruct>`).
- **Operator Overloading:** SystemC overloads the `<<` operator for sensitivity lists and port binding, and the `=` operator for reading/writing signals. It is not bit-shifting; it is C++ operator overloading.

## 4. The Standard Template Library (STL)
You will not write your own linked lists. You will use the STL.
- `std::vector` for dynamic arrays of ports or modules.
- `std::string` for dynamic module naming.
- `std::cout` and `std::endl` for simulation logging.

## Summary
If any of these concepts are entirely foreign to you, please pause and read through a comprehensive C++ programming guide. SystemC is incredibly powerful, but its power comes from leveraging advanced C++ paradigms to mimic hardware concurrency.

> [!IMPORTANT]
> This guide provides a conceptual overview. For production-level system design, always refer to the IEEE 1666-2023 standard and the Accellera proof-of-concept implementation headers to understand the underlying C++ template expansion.

Once you are comfortable with C++, proceed to the next tutorial to install the Accellera SystemC library.

---

## Lesson 2: Installation & Setup

Canonical lesson: https://www.learn-systemc.com/tutorials/002-installation-and-setup

How to download, compile, and link the Accellera SystemC library on Linux and Windows.

# Downloading and Installing SystemC

Since SystemC is a C++ library, you do not install an "executable" or an "IDE". You must download the source code, compile it into a static library (`.a` or `.lib`), and then link your own C++ code against it.

The official reference implementation is maintained by the **Accellera Systems Initiative**.

## Standard and source context

For this foundation lesson, keep three references close: `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for portable semantics, `Accellera SystemC GitHub repository` for kernel behavior, and `Accellera SystemC GitHub repository` for bit-accurate C++ types. When this lesson mentions a macro or type, the useful habit is to ask which C++ class the macro eventually creates.

## Step 1: Download the Source Code
1. Go to the [Accellera Systems Initiative Download Page](https://systemc.org/).
2. Download the latest **SystemC Core** source code (SystemC 3.0.1 or higher).
3. Extract the `.tar.gz` or `.zip` file to a permanent directory on your machine (e.g., `/opt/systemc` on Linux or `C:\systemc` on Windows).

---

## Step 2: Compiling the Library

### On Linux / macOS (Using CMake & GCC/Clang)
Modern SystemC provides a `CMakeLists.txt` making installation vastly easier than the old `autotools` method.

1. Open your terminal and navigate to the extracted SystemC directory:
   ```bash
   cd /path/to/systemc-3.0.1
   ```
2. Create a build directory and run CMake:
   ```bash
   mkdir build && cd build
   cmake .. -DCMAKE_CXX_STANDARD=17
   ```
3. Compile and install (this usually installs to `/usr/local/systemc` by default, or the path specified via `CMAKE_INSTALL_PREFIX`):
   ```bash
   make -j4
   sudo make install
   ```

### On Windows (Using Visual Studio)
1. Open the extracted folder and navigate to the `msvc10` or `msvc14` directory (depending on your Visual Studio version).
2. Open the `SystemC.sln` solution file in Visual Studio.
3. Select your desired configuration at the top: **Debug** or **Release**, and **x64**.
4. Right-click the `SystemC` project in the Solution Explorer and click **Build**.
5. Once finished, the compiled `.lib` files will be located in the `Debug` or `Release` folder within the MSVC directory.

---

## Step 3: Compiling Your First Program

Once the library is built, you can write a `main.cpp` file:

```cpp
#include <systemc.h>

int sc_main(int argc, char* argv[]) {
    std::cout << "Hello, SystemC World!" << std::endl;
    return 0;
}
```

### Compiling on Linux (g++)
You must tell the compiler where the SystemC headers (`-I`) and library files (`-L`) are located, and explicitly link the `systemc` library (`-lsystemc`).

```bash
g++ main.cpp -o hello_systemc \
    -I/usr/local/systemc/include \
    -L/usr/local/systemc/lib-linux64 \
    -lsystemc -lm
```

### Compiling on Windows (Visual Studio)
In your own project properties:
1. **C/C++ -> General -> Additional Include Directories:** Add `C:\systemc\src`.
2. **C/C++ -> Preprocessor -> Preprocessor Definitions:** Add `_CRT_SECURE_NO_WARNINGS`.
3. **Linker -> General -> Additional Library Directories:** Add `C:\systemc\msvc14\SystemC\x64\Release`.
4. **Linker -> Input -> Additional Dependencies:** Add `SystemC.lib`.

## Using CMake (Recommended)
Instead of typing out GCC flags or clicking through Visual Studio menus, the industry standard is to use **CMake** to build your projects. You can write a `CMakeLists.txt` based on the example in the previous step and use it for any future tutorials.

## Lesson 3: Official Resources Hub

Canonical lesson: https://www.learn-systemc.com/tutorials/003-official-resources-hub

Links to the IEEE Language Reference Manual, Accellera Forums, and official GitHub repositories.

# Official SystemC Resources

While LearnSystemC aims to be your definitive guide and single source of truth, it is crucial to know where the official standards and community discussions reside.

SystemC is an open standard maintained by the **Accellera Systems Initiative** and standardized by the **IEEE**.

## Standard and source context

For this foundation lesson, keep three references close: `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for portable semantics, `Accellera SystemC GitHub repository` for kernel behavior, and `Accellera SystemC GitHub repository` for bit-accurate C++ types. When this lesson mentions a macro or type, the useful habit is to ask which C++ class the macro eventually creates.

## 1. The Language Reference Manual (LRM)
The final authority on how SystemC behaves is the IEEE 1666 Standard. If you ever want to know the exact mathematical or logical rules governing the simulator, read the LRM.

- [IEEE Standard 1666-2023 (Latest)](https://standards.ieee.org/ieee/1666/10940/)
- [IEEE Standard 1666.1-2016 (CCI Standard)](https://standards.ieee.org/ieee/1666.1/6334/)

## 2. The Accellera Community Forums
The Accellera forums are the primary gathering place for SystemC engineers, tool vendors, and language architects. If you have a highly specific bug or compiler issue, search the forums first.

- [Accellera SystemC Community Forums](https://forums.accellera.org/forum/9-systemc/)
- [TLM Discussion Forum](https://forums.accellera.org/forum/12-tlm/)
- [SystemC AMS Discussion Forum](https://forums.accellera.org/forum/13-systemc-ams/)

## 3. Official Source Code Repositories
The Accellera Proof-of-Concept (PoC) simulator and associated libraries are open source and hosted on GitHub. Browsing these repositories is highly recommended once you reach the advanced chapters of LearnSystemC.

- **SystemC Core:** [github.com/accellera-official/systemc](https://github.com/accellera-official/systemc)
- **Configuration, Control & Inspection (CCI):** [github.com/accellera-official/cci](https://github.com/accellera-official/cci)
- **SystemC Verification Library (SCV):** [github.com/accellera-official/scv](https://github.com/accellera-official/scv)
- **UVM-SystemC:** [github.com/accellera-official/uvm-systemc](https://github.com/accellera-official/uvm-systemc)

Bookmark these links! They will be invaluable as you transition from a SystemC learner to a professional ESL architect.

## Lesson 4: Introduction to SystemC

Canonical lesson: https://www.learn-systemc.com/tutorials/004-introduction-to-systemc

What SystemC is, where it fits, and the mental model behind C++ hardware simulation.

## How to Read This Lesson

Read this like a conversation between normal C++ and the SystemC kernel. Whenever something looks like magic, ask: what C++ object did that macro or constructor register?

SystemC is a C++ class library and simulation kernel for modeling systems whose behavior is naturally concurrent: processors, buses, accelerators, interconnects, memories, peripherals, firmware-visible registers, and virtual platforms.

It is not a replacement syntax for Verilog or VHDL. It is C++ used with a hardware-oriented library. The library gives you modules, ports, signals, events, time, processes, and transaction-level modeling. Your compiler still sees C++, but the SystemC kernel sees a network of objects that can be elaborated, scheduled, and simulated.

## Standard and source context

Keep two references beside you as you read this chapter. The standard, `Docs/LRMs/SystemC_LRM_1666-2023.pdf`, tells you what a portable SystemC model is allowed to rely on. The Accellera source shows how those rules become C++ objects and scheduler code. For this first pass, follow just a small thread: `sc_module` gives the model a place in the hierarchy, `sc_object` gives it a name and identity, `sc_module_name` helps construction work cleanly, and `sc_simcontext` is the kernel object that eventually runs the simulation.

## Why SystemC Exists

Hardware teams often need answers before RTL exists:

- Does this architecture have enough memory bandwidth?
- Can firmware boot before the chip is built?
- Which DMA shape gives the best latency?
- How much timing detail is needed for a performance question?
- Can a testbench drive a model at transaction level before signal-level detail is ready?

SystemC fills that space by letting you model at different abstraction levels. You can write a cycle-accurate block, a loosely timed bus model, or a fast functional model in the same language.

## The Core Mental Model

A SystemC executable has two lives. First, normal C++ constructs objects. Then the SystemC kernel elaborates those objects into a simulation hierarchy and runs registered processes.

```cpp
#include <systemc>
using namespace sc_core;

SC_MODULE(Hello) {
  SC_CTOR(Hello) {
    SC_METHOD(say_hello);
  }

  void say_hello() {
    std::cout << "hello at " << sc_time_stamp() << "\n";
  }
};

int sc_main(int, char*[]) {
  Hello top{"top"};
  sc_start();
  return 0;
}
```

The important part is not the greeting. It is the registration. `SC_METHOD(say_hello)` tells the kernel that a member function is a process. `sc_start()` hands control to the scheduler. From that point on, time, events, and process readiness decide what runs.

## SystemC Is Useful Because It Is Layered

At the low level, you can model signals, clocks, and sensitivity like RTL. At the system level, you can model a memory transaction as a function call with a delay. Between those extremes, you can decide how much timing detail is worth paying for.

That is the design tradeoff this site keeps returning to: use the simplest model that answers the engineering question, then add detail where the question demands it.

## What This Course Covers

This site is organized like a long-form tutorial:

- C++ setup and the shape of a SystemC program
- Modules, hierarchy, constructors, and elaboration
- Processes, sensitivity, events, waits, and delta cycles
- Ports, interfaces, exports, channels, and binding
- Signals, resolved signals, clocks, and writer policies
- TLM-2.0 payloads, sockets, timing, and protocol phases
- Source-code reading: scheduler, signals, ports, exports, sockets, and process control
- Practical patterns for virtual platforms and deployable documentation

The source-code chapters point to the official Accellera reference implementation at [github.com/accellera-official/systemc](https://github.com/accellera-official/systemc). You do not need to memorize every private member. The goal is to recognize the architecture behind the public API.

## Lesson 5: Build SystemC and Write a First Model

Canonical lesson: https://www.learn-systemc.com/tutorials/005-build-systemc-and-write-a-first-model

How a SystemC program is compiled, linked, elaborated, and started.

## How to Read This Lesson

Read this like a conversation between normal C++ and the SystemC kernel. Whenever something looks like magic, ask: what C++ object did that macro or constructor register?

A SystemC model is a normal C++ program linked with the SystemC library. That makes the workflow familiar: include headers, compile sources, link against the library, and run the executable.

The official downloads and release material live under Accellera's SystemC resources, while active source development is public in the Accellera GitHub repository. In a production environment, pin a SystemC version the same way you would pin a compiler or simulator.

## Standard and source context

This first runnable model is where the standard stops being abstract. Use `Docs/LRMs/SystemC_LRM_1666-2023.pdf` to check what the kernel promises during elaboration and simulation startup, then compare that with the Accellera source path around `sc_module`, `sc_object`, `sc_module_name`, `sc_simcontext`, and process registration. The useful question is not "where is the magic?" but "which C++ object did this line register with the kernel?"

## Minimal Build Shape

The exact commands vary by installation, but the structure is consistent:

```bash
c++ -std=c++17 main.cpp \
  -I/path/to/systemc/include \
  -L/path/to/systemc/lib \
  -lsystemc \
  -o sim
./sim
```

Many teams wrap this with CMake:

```cmake
cmake_minimum_required(VERSION 3.20)
project(counter_systemc CXX)

set(CMAKE_CXX_STANDARD 17)
find_package(SystemCLanguage CONFIG REQUIRED)

add_executable(sim main.cpp)
target_link_libraries(sim SystemC::systemc)
```

## A Clocked Counter

This example introduces a module with input and output ports:

```cpp
#include <systemc>
using namespace sc_core;

SC_MODULE(Counter) {
  sc_in<bool> clk{"clk"};
  sc_out<unsigned> value{"value"};
  unsigned internal = 0;

  void tick() {
    value.write(++internal);
  }

  SC_CTOR(Counter) {
    SC_METHOD(tick);
    sensitive << clk.pos();
    dont_initialize();
  }
};

int sc_main(int, char*[]) {
  sc_clock clk{"clk", 10, SC_NS};
  sc_signal<unsigned> count{"count"};

  Counter counter{"counter"};
  counter.clk(clk);
  counter.value(count);

  sc_start(50, SC_NS);
  return 0;
}
```

`sc_clock` is a channel that provides a clock signal. `sc_signal<unsigned>` is a channel that stores a value and notifies readers when it changes. The `Counter` module exposes ports, and the top level binds those ports to channels.

## The Three Phases You Should Name

SystemC execution is easier to understand if you separate it into three phases:

1. **Construction**: C++ constructors allocate modules, channels, and local state.
2. **Elaboration**: SystemC finalizes hierarchy, port bindings, process registration, and object names.
3. **Simulation**: `sc_start()` lets the kernel run processes according to events and time.

Many confusing errors come from doing phase-specific work in the wrong place. Binding belongs before simulation. `wait()` belongs in a thread process during simulation. Creating a process dynamically is possible, but you should first learn the static model.

## Practical Advice

Keep the first model small. Build one clock, one signal, one module, and one print statement. Once that is working, add hierarchy. Then add events. Then add TLM. A SystemC environment is just C++, but debugging gets much easier when each layer has been proven independently.

## Lesson 6: Modules, Hierarchy, and Elaboration

Canonical lesson: https://www.learn-systemc.com/tutorials/006-modules-hierarchy-and-elaboration

How SC_MODULE, constructors, object names, and hierarchy registration shape the simulation.

## How to Read This Lesson

`SC_MODULE` is a convenience macro around a C++ class derived from `sc_module`. It exists because SystemC needs more than C++ object construction. The kernel also needs names, hierarchy, and process registration.

```cpp
#include <systemc>
using namespace sc_core;

SC_MODULE(Producer) {
  sc_out<int> out{"out"};

  SC_CTOR(Producer) {
    SC_THREAD(run);
  }

  void run() {
    for (int i = 0; i != 4; ++i) {
      out.write(i);
      wait(10, SC_NS);
    }
  }
};

SC_MODULE(Consumer) {
  sc_in<int> in{"in"};

  SC_CTOR(Consumer) {
    SC_METHOD(process);
    sensitive << in;
    dont_initialize();
  }

  void process() {
    std::cout << "Consumer received: " << in.read()
              << " at " << sc_time_stamp() << std::endl;
  }
};

SC_MODULE(Top) {
  sc_signal<int> data{"data"};
  Producer producer{"producer"};
  Consumer consumer{"consumer"};

  SC_CTOR(Top) {
    producer.out(data);
    consumer.in(data);
  }
};

int sc_main(int, char*[]) {
  Top top{"top"};
  sc_start(100, SC_NS);
  return 0;
}
```

The macro form is popular because it is compact. You can also write explicit C++ classes derived from `sc_module`, which is useful when templates or inheritance become more important than brevity.

## Standard and source context

## Names Matter

Every SystemC object has a name in the hierarchy. These names show up in reports, traces, and errors. A module created as `Producer producer{"producer"}` becomes part of the object tree. A port named `out` becomes `producer.out`.

Good names are not cosmetic. They are your debugging map.

## Elaboration Is the Build Step Inside the Executable

During elaboration, the kernel discovers the object hierarchy and checks structural rules. This is where port binding errors often appear. For example, an unbound port may not fail at C++ compile time, because the compiler only sees objects and function calls. The SystemC kernel detects whether the model graph is valid.

## Child Modules

Hierarchy is just composition:

Top modules construct their children and manage connections.
(Note: The definitions of Producer, Consumer, and Top are already included in the full example above.)

Construct child modules before binding them. This style keeps topology in the parent constructor, where readers expect to find it.

## Source-Code Angle

In the reference implementation, module construction participates in a global simulation context. The context keeps track of the current hierarchy scope while constructors run. That is how a child object can learn where it belongs without you manually passing a full hierarchical path into every port, channel, and module.

The big idea: SystemC uses ordinary C++ construction, but overlays a hierarchy-tracking discipline on top of it.

## Lesson 7: Processes, Events, and Time

Canonical lesson: https://www.learn-systemc.com/tutorials/007-processes-events-and-time

SC_METHOD, SC_THREAD, sensitivity, wait(), sc_event, and the meaning of delta cycles.

## How to Read This Lesson

Processes are the executable behavior inside a SystemC model. Modules provide structure. Channels provide communication. Processes provide activity.

SystemC has two process styles you will use constantly:

- `SC_METHOD`: runs to completion and cannot call `wait()`.
- `SC_THREAD`: can suspend with `wait()` and resume later.

## Standard and source context

## SC_METHOD

Use `SC_METHOD` for combinational behavior or small reactions to events:

```cpp
#include <systemc>
using namespace sc_core;

SC_MODULE(AndGate) {
  sc_in<bool> a{"a"};
  sc_in<bool> b{"b"};
  sc_out<bool> y{"y"};

  void comb() {
    y.write(a.read() && b.read());
  }

  SC_CTOR(AndGate) {
    SC_METHOD(comb);
    sensitive << a << b;
  }
};

int sc_main(int, char*[]) {
  sc_signal<bool> sig_a, sig_b, sig_y;
  AndGate and_gate("and_gate");
  and_gate.a(sig_a);
  and_gate.b(sig_b);
  and_gate.y(sig_y);

  sc_start(1, SC_NS);
  return 0;
}
```

The method runs when an event in its sensitivity list occurs. It should finish quickly because it cannot yield.

## SC_THREAD

Use `SC_THREAD` when behavior has an internal timeline:

```cpp
#include <systemc>
using namespace sc_core;

SC_MODULE(Timer) {
  sc_event done;

  SC_CTOR(Timer) {
    SC_THREAD(run);
  }

  void run() {
    wait(100, SC_NS);
    done.notify();
    std::cout << "Timer done at " << sc_time_stamp() << std::endl;
  }
};

int sc_main(int, char*[]) {
  Timer timer("timer");
  sc_start(200, SC_NS);
  return 0;
}
```

The call to `wait()` suspends the thread process. The simulation kernel saves enough process state to resume it when the wait condition is satisfied.

## Events Do Not Store History

An `sc_event` is not a queue of messages. It is a notification mechanism. If nobody is waiting when an immediate event is notified, the event is missed.

That makes this pattern important:

```cpp
#include <systemc>
using namespace sc_core;

SC_MODULE(Consumer) {
  sc_event producer_done;

  SC_CTOR(Consumer) {
    SC_THREAD(consumer_thread);
  }

  void consumer_thread() {
    while (true) {
      wait(producer_done);
      consume_result();
    }
  }

  void consume_result() {
    std::cout << "Result consumed at " << sc_time_stamp() << std::endl;
  }
};

int sc_main(int, char*[]) {
  Consumer cons("cons");
  cons.producer_done.notify(10, SC_NS);
  sc_start(20, SC_NS);
  return 0;
}
```

The process arms the wait first, then reacts.

## Delta Cycles

A delta cycle is a zero-time scheduling step. It lets the kernel settle chains of events without advancing simulation time. Signal writes use this idea: a process writes a new value, the channel schedules an update, and dependent processes wake in a later delta cycle.

Delta cycles are why SystemC can avoid many order-dependent bugs. Processes can run in a deterministic simulation order while still modeling hardware-like simultaneous updates.

## Practical Debug Rule

When behavior looks one step late, ask which phase you are observing:

- Did a method write a signal but the update has not happened yet?
- Did an event notify in the same delta or the next delta?
- Is a thread waiting on a value change or on a timed delay?

Most SystemC timing surprises become ordinary once you separate time advancement from delta-cycle settling.

## Under the Hood: `sc_process`, `sc_event`, and QuickThreads
When you register a process using `SC_METHOD` or `SC_THREAD`, SystemC allocates a process object inheriting from `sc_process_b` (defined in `sysc/kernel/sc_process.h`).
- **`sc_method_process`**: A simple C++ function pointer. The scheduler calls it, and it must run to completion.
- **`sc_thread_process`**: Requires its own execution stack to support `wait()`. Under the hood, the Accellera kernel uses a coroutine library. On Linux/Windows, it typically uses QuickThreads (`src/sysc/qt/`) or POSIX fibers. When `wait()` is called, the coroutine context is saved, and execution yields back to the SystemC scheduler.
When an `sc_event::notify()` is called, the kernel pushes the event into `sc_simcontext::m_event_list`. At the end of the delta cycle, the scheduler wakes up all processes statically or dynamically sensitive to that event by moving them into the `m_runnable` list.

## Lesson 8: Datatypes and Bit-Accurate Modeling

Canonical lesson: https://www.learn-systemc.com/tutorials/008-datatypes-and-bit-accurate-modeling

When to use C++ integers, sc_int, sc_uint, sc_bigint, sc_bv, sc_lv, fixed-point types, and enums.

## How to Read This Lesson

SystemC is C++, so the first datatype question is always: can a normal C++ type answer this modeling question? If yes, use it. If the model needs hardware-shaped behavior, use SystemC datatypes. In accordance with the LRM Section 7, SystemC provides a robust set of numeric and vector types.

Here is a complete, fully compilable example demonstrating how all these types are instantiated and used within a SystemC module:

```cpp
#include <systemc>
// Include the specific headers for SystemC datatypes
#include <sysc/datatypes/int/sc_int.h>
#include <sysc/datatypes/int/sc_uint.h>
#include <sysc/datatypes/bit/sc_bv.h>
#include <sysc/datatypes/bit/sc_lv.h>
#include <sysc/datatypes/fx/sc_fixed.h>

using namespace sc_core;

enum class BusState {
  Idle,
  Address,
  Data,
  Response,
};

SC_MODULE(DatatypeDemo) {
  SC_CTOR(DatatypeDemo) {
    SC_THREAD(run);
  }

  void run() {
    // Native C++ Types: fast and familiar
    uint32_t address = 0x40001000;
    uint8_t byte = 0xff;
    bool irq_pending = false;

    // sc_int and sc_uint: EXACT small bit-widths (up to 64 bits)
    sc_dt::sc_uint<12> page_offset = address & 0x0FFF;
    sc_dt::sc_int<9> signed_delta = -7;
    std::cout << "Page offset: " << page_offset << ", signed delta: " << signed_delta << "\n";

    // sc_bigint and sc_biguint: Arbitrary precision (> 64 bits)
    sc_dt::sc_biguint<257> wide_accumulator = 0;
    wide_accumulator = wide_accumulator + 1;

    // sc_bv and sc_lv: Bit vectors and Logic vectors
    sc_dt::sc_bv<8> mask = "10101100";
    sc_dt::sc_lv<4> bus = "10ZX"; // Z = High impedance, X = Unknown
    std::cout << "Bit vector mask: " << mask << ", Logic vector bus: " << bus << "\n";

    // Fixed-Point Types: DSP and quantization
    // <Total word length, Integer word length>
    sc_dt::sc_fixed<16, 2> gain = 1.25;
    std::cout << "Fixed point gain: " << gain << "\n";

    // Enums
    BusState state = BusState::Idle;
    if (state == BusState::Idle) {
       std::cout << "Bus is idle.\n";
    }
  }
};

int sc_main(int argc, char* argv[]) {
  DatatypeDemo demo("demo");
  sc_start();
  return 0;
}
```

## Standard and source context

## Choosing a Type

Use this practical rule:

- Use native C++ types for fast functional state.
- Use `sc_uint` and `sc_int` for exact small widths.
- Use bit vectors (`sc_bv`) for bit slicing and packed fields when arithmetic isn't the primary goal.
- Use logic vectors (`sc_lv`) when `X` or `Z` is meaningful for bus resolution (LRM Section 7.9).
- Use fixed-point types (`sc_fixed`) for quantization and DSP modeling.
- Use enums for readable control state.

The best type is the one that preserves the behavior you need without pretending the model is more detailed than it is.

## Under the Hood: `sc_logic` and 4-Value Logic
SystemC's `sc_logic` provides 4-value logic: `'0'`, `'1'`, `'Z'` (high impedance), and `'X'` (unknown).
In `sysc/datatypes/bit/sc_logic.h`, these states are represented by the enum `sc_logic_value_t`.
When you perform operations on `sc_logic`, the library uses lookup tables (arrays) for logic gates. For example, the AND operation `a & b` uses a 4x4 matrix where `X AND 0` yields `0`, but `X AND 1` yields `X`. This precise modeling is vital for RTL co-simulation, but it comes at a significant simulation performance cost compared to native boolean math.

## Lesson 9: Reset and Clocked Processes

Canonical lesson: https://www.learn-systemc.com/tutorials/009-reset-and-clocked-processes

How to model reset, clocked behavior, dont_initialize(), async_reset_signal_is(), and reset_signal_is().

## How to Read This Lesson

Reset modeling looks simple until the model mixes methods, threads, clocks, and initialization. Be explicit about what kind of reset you are modeling. According to the IEEE 1666 LRM Section 5.2.2, resets can be synchronous or asynchronous, and the behavior applies differently depending on the process type.

Here is a complete compilable example demonstrating both method-based synchronous resets and thread-based asynchronous resets.

```cpp
#include <systemc>

using namespace sc_core;

SC_MODULE(ResetDemo) {
  sc_in<bool> clk{"clk"};
  sc_in<bool> rst_sync{"rst_sync"};
  sc_in<bool> rst_async{"rst_async"};

  sc_out<int> count_method{"count_method"};
  sc_out<int> count_thread{"count_thread"};

  SC_CTOR(ResetDemo) {
    // 1. Method-Based Clocked Logic
    SC_METHOD(tick_method);
    sensitive << clk.pos();
    dont_initialize();

    // 2. Thread-Based Clocked Logic with explicit reset routing
    SC_THREAD(run_thread);
    sensitive << clk.pos();
    async_reset_signal_is(rst_async, true);
  }

  // Synchronous Reset modeled inside the body
  void tick_method() {
    if (rst_sync.read()) {
      count_method.write(0);
      return;
    }
    count_method.write(count_method.read() + 1);
  }

  // Asynchronous Reset handled by the kernel
  void run_thread() {
    // This code block acts as the reset initialization
    count_thread.write(0);
    wait();

    while (true) {
      // Normal clocked behavior
      count_thread.write(count_thread.read() + 1);
      wait();
    }
  }
};

int sc_main(int argc, char* argv[]) {
  sc_clock clk("clk", 10, SC_NS);
  sc_signal<bool> rst_sync("rst_sync");
  sc_signal<bool> rst_async("rst_async");
  sc_signal<int> count_m("count_m"), count_t("count_t");

  ResetDemo demo("demo");
  demo.clk(clk);
  demo.rst_sync(rst_sync);
  demo.rst_async(rst_async);
  demo.count_method(count_m);
  demo.count_thread(count_t);

  // Assert both resets
  rst_sync.write(true);
  rst_async.write(true);
  sc_start(15, SC_NS);

  // De-assert and run
  rst_sync.write(false);
  rst_async.write(false);
  sc_start(50, SC_NS);

  // Assert async reset mid-flight
  rst_async.write(true);
  sc_start(20, SC_NS);

  return 0;
}
```

## Standard and source context

## Method-Based Clocked Logic

A common method process reacts to a clock edge. `dont_initialize()` matters because SystemC normally initializes method processes once before simulation time advances. For clocked logic, that time-zero call is often not desired.

For a method process, synchronous reset is usually modeled explicitly in the body, checking the reset signal state before updating logic.

## Thread-Based Clocked Logic

Threads can express sequential control more naturally. The exact reset behavior depends on the process kind and reset declaration. When using `async_reset_signal_is(rst, true)` or `reset_signal_is(rst, true)`, the simulation kernel tracks the reset signal. If the signal goes active, the kernel throws a specific C++ exception inside the process to immediately unwind the stack and jump back to the beginning of the `run` method!

## Avoid Hidden State Surprises

Local C++ variables inside a thread preserve state across waits:

```cpp
void run() {
  unsigned local_count = 0;
  while (true) {
    out.write(local_count++);
    wait();
  }
}
```

If the reset restarts the process via `async_reset_signal_is`, local variables inside the while loop will be destroyed, and the process restarts from the top of the function where `local_count = 0` is executed again.

## Modeling Advice

Keep reset code boring:
- Put all reset assignments in one obvious branch.
- Decide whether reset affects only architectural state or also modeling statistics.
- Test reset assertion, deassertion, and reset during activity (mid-flight).

## Under the Hood: `sc_reset` and `sc_unwind_exception`
When you define a reset using `reset_signal_is(rst, active_high)`, you are attaching a reset policy to the `sc_process_b` object.
How does the thread actually reset? During the evaluation phase, if the kernel detects that the reset signal has matched its active state, it throws a C++ exception of type `sc_unwind_exception` *inside* the running thread context (`sysc/kernel/sc_process.cpp`).
This exception aggressively unwinds the C++ stack of the coroutine. In your thread code, the `wait()` call throws this exception. SystemC catches it at the root of the thread wrapper, resets the local variables, and restarts the thread function from the beginning.

## Lesson 10: Ports, Interfaces, Exports, and Channels

Canonical lesson: https://www.learn-systemc.com/tutorials/010-ports-interfaces-exports-and-channels

How modules connect through interfaces, why ports are typed, and where sc_export fits.

## How to Read This Lesson

Think of this chapter as wiring discipline. Ports, exports, interfaces, and channels are not decorative; they are how the model states its contract before time starts moving.

SystemC communication is built around interfaces. A port requires an interface. A channel implements an interface. An export exposes an interface from inside a module.

That separation is one of the most important design choices in SystemC.

## Standard and source context

The standard contract lives in `Docs/LRMs/SystemC_LRM_1666-2023.pdf` around interfaces, ports, exports, primitive channels, hierarchical channels, and predefined channels. The implementation trail is `Accellera SystemC GitHub repository`: `sc_port`, `sc_export`, `sc_interface`, `sc_prim_channel`, `sc_signal`, `sc_fifo`, and the writer policy helpers.

## Interfaces

An interface is an abstract contract:

```cpp
#include <systemc>

struct BusIf : sc_core::sc_interface {
  virtual int read(unsigned address) = 0;
  virtual void write(unsigned address, int data) = 0;
};
```

The interface says what can be done. It does not say how storage, timing, arbitration, tracing, or contention are implemented.

## Channels

A channel implements the interface. Let's make a complete compilable example of a memory channel. Note how it inherits from `sc_core::sc_channel` and `BusIf`:

```cpp
#include <systemc>

struct BusIf : sc_core::sc_interface {
  virtual int read(unsigned address) = 0;
  virtual void write(unsigned address, int data) = 0;
};

class SimpleMemory : public sc_core::sc_channel, public BusIf {
public:
  int data[256]{};

  SC_CTOR(SimpleMemory) {}

  int read(unsigned address) override {
    return data[address & 0xff];
  }

  void write(unsigned address, int value) override {
    data[address & 0xff] = value;
  }
};

int sc_main(int, char*[]) {
  SimpleMemory mem("mem");
  mem.write(0x0, 42);
  return 0;
}
```

This channel is untimed and simple. Later you could add `wait()` in a thread-aware interface, model bus latency, or record accesses for debugging.

## Ports and Exports

A module uses a port to call an interface, and an export lets a module provide an interface implemented by a child object. Here is a complete model putting it all together:

```cpp
#include <systemc>

struct BusIf : sc_core::sc_interface {
  virtual int read(unsigned address) = 0;
  virtual void write(unsigned address, int data) = 0;
};

class SimpleMemory : public sc_core::sc_channel, public BusIf {
public:
  int data[256]{};
  SC_CTOR(SimpleMemory) {}
  int read(unsigned address) override { return data[address & 0xff]; }
  void write(unsigned address, int value) override { data[address & 0xff] = value; }
};

SC_MODULE(CpuModel) {
  sc_core::sc_port<BusIf> bus{"bus"};

  SC_CTOR(CpuModel) {
    SC_THREAD(run);
  }

  void run() {
    bus->write(0x10, 7);
    int value = bus->read(0x10);
    std::cout << "CPU Read value: " << value << " at time " << sc_core::sc_time_stamp() << "\n";
  }
};

SC_MODULE(MemorySubsystem) {
  sc_core::sc_export<BusIf> target{"target"};
  SimpleMemory mem{"mem"};

  SC_CTOR(MemorySubsystem) {
    target.bind(mem);
  }
};

int sc_main(int, char*[]) {
  CpuModel cpu("cpu");
  MemorySubsystem mem_sys("mem_sys");

  cpu.bus.bind(mem_sys.target);
  sc_core::sc_start();
  return 0;
}
```

The port is typed by the interface, not by the concrete channel. This keeps the CPU model from depending on a particular memory implementation. The parent module exposes `target`, while the real implementation lives in `mem`. This is useful for hierarchy: external modules bind to the subsystem, while internal structure remains hidden.

## Source-Code Angle

In the reference implementation, ports and exports are part of a binding system layered over C++ pointers and interface references. The kernel validates that a required interface exists and that the final object graph is consistent before simulation begins.

That is why binding errors are usually elaboration errors, not C++ type errors alone.

## Under the Hood: Port Binding and Virtual Interfaces
A port (`sc_core::sc_port`) is fundamentally a safe wrapper around a C++ pointer to an interface (`sc_core::sc_interface`).
When you bind a port `p(signal)`, the `sc_port_base::bind()` method is invoked (`sysc/communication/sc_port.cpp`). The kernel does not immediately resolve the binding. Instead, it adds the binding pair to a list.
During the `complete_binding()` phase, the kernel walks through the connections. If multiple ports are bound hierarchically (Port -> Port -> Channel), the kernel traverses the chain until it finds the actual `sc_interface` implementation. It then caches this interface pointer directly inside the port.
Thus, calling `p->read()` has almost zero overhead during simulationâ€”it is just a standard C++ virtual function call (`interface_ptr->read()`).

## Lesson 11: Signals, Clocks, and Primitive Channels

Canonical lesson: https://www.learn-systemc.com/tutorials/011-signals-clocks-and-primitive-channels

How sc_signal stores values, delays updates, notifies readers, and models hardware-like behavior.

## How to Read This Lesson

Think of this chapter as wiring discipline. Ports, exports, interfaces, and channels are not decorative; they are how the model states its contract before time starts moving.

`sc_signal<T>` is the everyday channel for value communication. It implements signal interfaces, stores a current value, accepts writes, and notifies readers when the value changes.

## Standard and source context

The standard contract lives in `Docs/LRMs/SystemC_LRM_1666-2023.pdf` around interfaces, ports, exports, primitive channels, hierarchical channels, and predefined channels. The implementation trail is `Accellera SystemC GitHub repository`: `sc_port`, `sc_export`, `sc_interface`, `sc_prim_channel`, `sc_signal`, `sc_fifo`, and the writer policy helpers.

## Read and Write

```cpp
#include <systemc>
using namespace sc_core;

SC_MODULE(Writer) {
  sc_out<int> out{"out"};
  SC_CTOR(Writer) { SC_THREAD(run); }
  void run() {
    out.write(42);
    wait(10, SC_NS);
  }
};

int sc_main(int, char*[]) {
  sc_signal<int> data{"data"};
  Writer w("writer");
  w.out(data);

  data.write(42);
  int old_or_new = data.read();

  sc_start(20, SC_NS);
  return 0;
}
```

The tricky part is timing. A write does not necessarily become visible immediately to all other processes. The signal requests an update from the kernel. During the update phase, the current value changes and value-change events are notified.

## Why Delayed Update Exists

Hardware does not usually behave like a sequence of software assignments. If two processes evaluate during the same simulated moment, the final state should not depend on an arbitrary function-call order.

Delayed update gives SystemC a hardware-like discipline:

1. Processes evaluate and request channel updates.
2. Primitive channels update.
3. Events from those updates wake dependent processes.
4. More delta cycles run if necessary.

This is the evaluate-update rhythm behind many SystemC semantics.

## Clocks

`sc_clock` is a predefined channel that toggles over time:

```cpp
#include <systemc>
using namespace sc_core;

SC_MODULE(ClockedModule) {
  sc_in<bool> clk{"clk"};
  SC_CTOR(ClockedModule) {
    SC_METHOD(tick);
    sensitive << clk.pos();
    dont_initialize();
  }
  void tick() {
    std::cout << "Tick at " << sc_time_stamp() << std::endl;
  }
};

int sc_main(int, char*[]) {
  sc_clock clk{"clk", 10, SC_NS};
  ClockedModule mod("mod");
  mod.clk(clk);
  sc_start(30, SC_NS);
  return 0;
}
```

`dont_initialize()` prevents the method from running once at time zero before the first triggering edge.

## Writer Policies

Signals can enforce writer rules. A common bug is accidentally driving the same signal from multiple processes. SystemC has writer-policy machinery to detect or allow different cases, depending on the signal type and configuration.

Resolved signals exist for cases such as tri-state or multi-driver logic, but you should not use them to hide accidental architecture problems. If a signal has multiple writers, make that a conscious design choice.

## Primitive Channels

Signals are primitive channels. They participate directly in the kernel update phase through `request_update()` and an `update()` callback. That source-code shape explains why writing a signal from a process is not the same as assigning a C++ variable.

The public API is small. The behavior comes from how the channel cooperates with the scheduler.

## Under the Hood: Evaluate and Update Phases in `sc_signal`
`sc_signal<T>` inherits from `sc_prim_channel`. This is crucial for the Evaluate-Update paradigm.
Inside `sc_signal`, there are two member variables representing state: `m_cur_val` (current value) and `m_new_val` (next value).
When a process writes to a signal (`sig.write(val)`), `m_new_val` is updated, and the channel calls `request_update()` (`sysc/kernel/sc_simcontext.cpp`). This registers the signal in the scheduler's `m_update_list`.
During the **Evaluate** phase, all runnable processes execute.
During the **Update** phase, the scheduler loops over `m_update_list` and calls `update()` on each primitive channel. `sc_signal::update()` assigns `m_cur_val = m_new_val` and, if the value changed, fires the `value_changed_event`, which schedules sensitive processes for the *next* delta cycle.

## Lesson 12: FIFOs, Mutexes, Semaphores, and Custom Channels

Canonical lesson: https://www.learn-systemc.com/tutorials/012-fifos-mutexes-semaphores-and-custom-channels

When to use built-in channels and how to design communication abstractions of your own.

## How to Read This Lesson

Think of this chapter as wiring discipline. Ports, exports, interfaces, and channels are not decorative; they are how the model states its contract before time starts moving.

Not every connection should be a signal. SystemC includes higher-level channels such as FIFOs, mutexes, and semaphores because many system models care more about transactions and resources than individual wires.

## Standard and source context

The standard contract lives in `Docs/LRMs/SystemC_LRM_1666-2023.pdf` around interfaces, ports, exports, primitive channels, hierarchical channels, and predefined channels. The implementation trail is `Accellera SystemC GitHub repository`: `sc_port`, `sc_export`, `sc_interface`, `sc_prim_channel`, `sc_signal`, `sc_fifo`, and the writer policy helpers.

## sc_fifo

`sc_fifo<T>` models queued communication:

```cpp
#include <systemc>
using namespace sc_core;

SC_MODULE(Producer) {
  sc_fifo_out<int> out{"out"};

  SC_CTOR(Producer) {
    SC_THREAD(run);
  }

  void run() {
    for (int i = 0; i != 8; ++i) {
      out.write(i);
      std::cout << "Wrote " << i << " at " << sc_time_stamp() << std::endl;
      wait(10, SC_NS);
    }
  }
};

SC_MODULE(Consumer) {
  sc_fifo_in<int> in{"in"};
  SC_CTOR(Consumer) { SC_THREAD(run); }
  void run() {
    while (true) {
      int val = in.read();
      std::cout << "Read " << val << " at " << sc_time_stamp() << std::endl;
    }
  }
};

int sc_main(int, char*[]) {
  sc_fifo<int> fifo(4);
  Producer p("p");
  Consumer c("c");
  p.out(fifo);
  c.in(fifo);
  sc_start(100, SC_NS);
  return 0;
}
```

The blocking `write()` waits when the FIFO is full. The blocking `read()` waits when the FIFO is empty. This is often perfect for modeling pipelines, queues, and producer-consumer systems.

## Mutexes and Semaphores

`sc_mutex` and `sc_semaphore` model shared resources. Use them when the model question is about arbitration or resource ownership.

```cpp
#include <systemc>
using namespace sc_core;

SC_MODULE(Master) {
  sc_port<sc_mutex_if> bus_lock{"bus_lock"};

  SC_CTOR(Master) { SC_THREAD(run); }

  void run() {
    bus_lock->lock();
    std::cout << name() << " got lock at " << sc_time_stamp() << std::endl;
    wait(20, SC_NS);
    bus_lock->unlock();
  }
};

int sc_main(int, char*[]) {
  sc_mutex mutex("mutex");
  Master m1("m1"), m2("m2");
  m1.bus_lock(mutex);
  m2.bus_lock(mutex);
  sc_start(50, SC_NS);
  return 0;
}
```

These are modeling constructs, not magic performance tools. Use them when they express the system behavior clearly.

## Custom Channels

Custom channels are where SystemC becomes a modeling framework rather than a fixed simulator. You define an interface, implement it in a channel, and bind modules to that interface.

Good custom channels hide policy:

- timing
- arbitration
- buffering
- tracing
- protocol conversion
- statistics

The module using the channel should care about the operation it needs, not the machinery behind it.

## Design Rule

If the connection is a wire, use a signal. If the connection is a transaction, use an interface or TLM socket. If the connection is a queue, use a FIFO. If the connection is a shared resource, use a mutex or semaphore. The model reads better when the channel matches the concept.

## Under the Hood: `sc_fifo` and `sc_mutex` Blocking Semantics
`sc_fifo<T>` and `sc_mutex` implement blocking synchronization using `sc_event` and `wait()`.
In `sysc/communication/sc_fifo.cpp`, if a thread calls `read()` but the FIFO is empty, the channel calls `wait(m_data_written_event)`. The thread is suspended. When another thread calls `write()`, it executes `m_data_written_event.notify(SC_ZERO_TIME)`. The blocked reader is added to the runnable queue and resumes in the next delta cycle.
Similarly, `sc_mutex::lock()` checks if the mutex is available. If not, it waits on `m_free_event`. When locked, the mutex stores the process handle (`sc_get_current_process_handle()`) of the owner to ensure only the owner can `unlock()` it, throwing an error if a different process attempts to release the lock.

## Lesson 13: Source Deep Dive: sc_export and Hierarchical Binding

Canonical lesson: https://www.learn-systemc.com/tutorials/013-source-deep-dive-sc-export-and-hierarchical-bindin

How exports expose interfaces upward, how hierarchical binding resolves, and why binding errors appear at elaboration end.

## How to Read This Lesson

Ports are how a module asks for an interface. Exports are how a module offers an interface from something inside it. If that feels backwards at first, imagine a submodule hiding a channel while still exposing the channel's API to its parent.

## Standard and source context

Use `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for interfaces, ports, exports, channels, and binding rules. In source, inspect `Accellera SystemC GitHub repository`, `sc_port.*`, `sc_interface.*`, and the binding completion logic connected through `sc_simcontext`.

## Why Exports Exist

Suppose a subsystem contains an internal FIFO, but the parent should connect to the subsystem rather than to the FIFO directly.

```cpp
SC_MODULE(Subsys) {
  sc_core::sc_export<sc_core::sc_fifo_in_if<int>> in_export;
  sc_core::sc_fifo<int> fifo;

  SC_CTOR(Subsys)
  : in_export("in_export"), fifo("fifo", 16) {
    in_export.bind(fifo);
  }
};
```

The export says: "from outside this module, you may treat me as this interface." The implementation object is still the internal channel.

## Port to Export to Channel

A parent can connect a port to the export:

```cpp
sc_core::sc_port<sc_core::sc_fifo_in_if<int>> in_port;
Subsys subsys{"subsys"};

in_port.bind(subsys.in_export);
```

At the API level, that looks like one bind. At elaboration completion, the kernel resolves the chain:

```text
port -> export -> fifo channel -> interface implementation
```

Then the port can call interface methods through the resolved interface pointer.

## How the Source Makes This Work

The implementation defers full binding checks until the end of elaboration. That is not laziness; it is what lets C++ constructors build a hierarchy in any practical order.

During construction:

- ports register with the port registry
- exports register as objects that can provide interfaces
- bind calls record relationships

During binding completion:

- hierarchical chains are resolved
- missing bindings are reported
- type mismatches are reported
- final interface pointers are cached

## Pitfall: Export Without an Interface

An export that is never bound to a concrete interface is not useful. It is like a public API endpoint with no implementation behind it.

Good review question: if this export is visible in the hierarchy, what channel or object actually implements its interface?

## Review Checklist

- Does every export bind to a concrete interface?
- Is the export exposing the smallest interface needed?
- Does the parent connect to the subsystem boundary instead of reaching into internals?
- Are binding errors caught during elaboration rather than hidden until runtime?
- Are names stable enough for hierarchy inspection and reports?

## Lesson 14: TLM-2.0, Generic Payloads, and Sockets

Canonical lesson: https://www.learn-systemc.com/tutorials/014-tlm-2-0-generic-payloads-and-sockets

The practical model for fast transaction-level platforms using initiator and target sockets.

## How to Read This Lesson

# TLM-2.0, Generic Payloads, and Sockets

Transaction Level Modeling (TLM) changes the unit of communication. Instead of toggling pins and evaluating clock edges delta-cycle by delta-cycle, a model sends a high-level **transaction**: read this address, write these bytes, wait this long, return this response.

TLM-2.0 is the official IEEE standard methodology built on top of SystemC to standardize how IPs communicate in Virtual Platforms, ensuring interoperability between models from different vendors.

## Standard and source context

## The Generic Payload (`tlm_generic_payload`)

The core of TLM-2.0 is the `tlm_generic_payload`. According to the LRM, this single class encapsulates all attributes necessary to model standard memory-mapped bus transactions (like AXI, AHB, APB, PCIe).

The standard payload carries:
- **Command:** Read (`TLM_READ_COMMAND`), Write (`TLM_WRITE_COMMAND`), or Ignore.
- **Address:** A 64-bit unsigned integer representing the memory-mapped address.
- **Data Pointer and Length:** An `unsigned char*` array and its length.
- **Byte Enables:** A pointer and length for masking specific bytes during writes.
- **Streaming Width:** Used to model burst transfers to a fixed FIFO address.
- **Response Status:** Updated by the target (e.g., `TLM_OK_RESPONSE`, `TLM_ADDRESS_ERROR_RESPONSE`).
- **Extensions:** A mechanism to attach custom bus-specific metadata (like AXI secure bits) without altering the base payload.

By enforcing a single generic payload, TLM-2.0 ensures that a CPU modeled by ARM can talk directly to a memory controller modeled by Synopsys without needing a protocol adapter.

## Initiator and Target Sockets

TLM-2.0 groups ports, exports, and interfaces into a unified concept called **Sockets**.
- **Initiator Socket:** Sends transactions (e.g., CPUs, DMAs).
- **Target Socket:** Receives transactions (e.g., RAMs, Peripherals).

The `tlm_utils` namespace provides `simple_initiator_socket` and `simple_target_socket`, which hide the complex interface multi-inheritance rules required by the LRM, making TLM-2.0 modeling highly accessible.

## Loosely Timed (LT) Modeling (Blocking Transport)

The most common modeling style for firmware development is **Loosely Timed (LT)** modeling, which uses the `b_transport` (blocking transport) interface.

In `b_transport`, the initiator calls a function on the target, passing the payload and a `sc_time` reference. The target performs the memory operation instantly in C++ execution time, updates the `sc_time` reference to indicate how long the hardware *would* have taken, and returns.

This skips thousands of simulated clock cycles instantly, enabling Virtual Platforms to boot Linux in seconds.

## Complete Example: Initiator to Target Communication

Here is a complete, compilable `sc_main` demonstrates a simple DMA (Initiator) writing data to a Memory block (Target) using standard TLM-2.0 blocking transport and generic payloads.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <iostream>

// ---------------------------------------------------------
// INITIATOR (e.g., CPU, DMA)
// ---------------------------------------------------------
SC_MODULE(Initiator) {
    tlm_utils::simple_initiator_socket<Initiator> socket{"socket"};

    SC_CTOR(Initiator) {
        SC_THREAD(run);
    }

    void run() {
        tlm::tlm_generic_payload trans;
        sc_core::sc_time delay = sc_core::SC_ZERO_TIME;

        // Data to write
        uint32_t data = 0xDEADBEEF;

        // Configure the Generic Payload
        trans.set_command(tlm::TLM_WRITE_COMMAND);
        trans.set_address(0x1000);
        trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
        trans.set_data_length(4);
        trans.set_streaming_width(4); // Required by LRM
        trans.set_byte_enable_ptr(nullptr); // No byte masking
        trans.set_dmi_allowed(false);
        trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

        std::cout << "@ " << sc_core::sc_time_stamp()
                  << " [Initiator] Sending WRITE to 0x1000.\n";

        // Perform the Blocking Transport call
        socket->b_transport(trans, delay);

        // Advance local time based on the target's annotated delay
        wait(delay);

        if (trans.is_response_error()) {
            SC_REPORT_ERROR("TLM", "Transaction returned error");
        } else {
            std::cout << "@ " << sc_core::sc_time_stamp()
                      << " [Initiator] Transaction Complete. Target consumed delay.\n";
        }
    }
};

// ---------------------------------------------------------
// TARGET (e.g., Memory, Peripheral)
// ---------------------------------------------------------
SC_MODULE(Target) {
    tlm_utils::simple_target_socket<Target> socket{"socket"};

    SC_CTOR(Target) {
        // Register the callback function for blocking transport
        socket.register_b_transport(this, &Target::b_transport);
    }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        tlm::tlm_command cmd = trans.get_command();
        uint64_t         adr = trans.get_address();

        if (cmd == tlm::TLM_WRITE_COMMAND) {
            std::cout << "  -> [Target] Processing WRITE at 0x" << std::hex << adr << "\n";
            // In a real model, we would copy trans.get_data_ptr() into our memory array here.
        }

        // LRM Mandate: Target must set response status
        trans.set_response_status(tlm::TLM_OK_RESPONSE);

        // Annotate how long this operation takes in hardware (e.g., 20ns memory access)
        delay += sc_core::sc_time(20, sc_core::SC_NS);
    }
};

// ---------------------------------------------------------
// TOP LEVEL
// ---------------------------------------------------------
int sc_main(int argc, char* argv[]) {
    Initiator init("init");
    Target    tgt("tgt");

    // Bind the sockets (Notice how simple it is compared to port-by-port binding)
    init.socket.bind(tgt.socket);

    std::cout << "Starting TLM-2.0 Simulation...\n";
    sc_core::sc_start();

    return 0;
}
```

### Explanation of the Execution

When you run this simulation, the output will be:

```
Starting TLM-2.0 Simulation...
@ 0 s [Initiator] Sending WRITE to 0x1000.
  -> [Target] Processing WRITE at 0x1000
@ 20 ns [Initiator] Transaction Complete. Target consumed delay.
```

Notice the power of the `delay` variable. The Target executes its C++ code instantly, but adds `20 ns` to the `delay` reference. When the transport call returns, the Initiator calls `wait(delay)`, advancing the simulation time to `20 ns`. This temporal decoupling is the secret to Virtual Platform simulation speed.

## Under the Hood: `tlm_generic_payload` and Memory Management
The TLM-2.0 `tlm_generic_payload` (GP) is designed for speed. In `src/tlm_core/tlm_2/tlm_generic_payload/tlm_gp.h`, you'll see it is a heavy class containing a standard set of attributes (command, address, data pointer, response status).
To prevent the immense overhead of allocating and deleting GP objects during millions of transactions, the standard mandates the use of a Memory Manager (`tlm_mm_interface`). The GP has a pointer `m_mm`. When a transaction completes and its reference count hits zero (`release()`), instead of calling `delete`, the `m_mm->free()` method is called, returning the GP to a pool for reuse.
Additionally, the GP contains an array of extension pointers (`tlm_extension_base* m_extensions[]`). This allows targets to attach custom metadata without modifying the GP structure or relying on slow `dynamic_cast`.

## Lesson 15: Non-Blocking TLM and Protocol Phases

Canonical lesson: https://www.learn-systemc.com/tutorials/015-non-blocking-tlm-and-protocol-phases

How BEGIN_REQ, END_REQ, BEGIN_RESP, and END_RESP express pipelined transaction protocols.

## How to Read This Lesson

Blocking transport models a transaction as one call. Non-blocking transport models a transaction as a sequence of phases. According to the IEEE 1666-2023 LRM Section 14, non-blocking transport is used when protocol ordering, pipelining, and response timing matter.

## Standard and source context

## The Four-Phase Base Protocol

The TLM-2.0 base protocol commonly uses:

- `BEGIN_REQ`: initiator begins a request
- `END_REQ`: target accepts the request
- `BEGIN_RESP`: target begins the response
- `END_RESP`: initiator accepts the response

This lets a target accept a request now, freeing the initiator to do other work, and complete it later. The initiator calls the forward path (`nb_transport_fw`), and the target can call back on the backward path (`nb_transport_bw`). The split reflects protocol direction.

## Return Status

Non-blocking transport returns a synchronization enum (`tlm_sync_enum`):

- `TLM_ACCEPTED`: transaction accepted, more phases will arrive later via the backward/forward path.
- `TLM_UPDATED`: phase and delay were updated immediately.
- `TLM_COMPLETED`: transaction completed without more callbacks.

The status is part of the protocol. Ignoring it is a violation of the TLM-2.0 standard.

## Payload Event Queue and Complete Example

`tlm_utils::peq_with_get` helps targets schedule transactions for later processing. A PEQ keeps the target from trying to complete everything inside the transport call.

Here is a fully compilable, standards-compliant AT (Approximately-Timed) non-blocking example utilizing the Payload Event Queue.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <tlm_utils/peq_with_get.h>

using namespace sc_core;

SC_MODULE(NBTarget) {
  tlm_utils::simple_target_socket<NBTarget> socket{"socket"};
  tlm_utils::peq_with_get<tlm::tlm_generic_payload> peq;

  SC_CTOR(NBTarget) : peq("peq") {
    socket.register_nb_transport_fw(this, &NBTarget::nb_transport_fw);
    SC_THREAD(process_transactions);
  }

  tlm::tlm_sync_enum nb_transport_fw(tlm::tlm_generic_payload& trans,
                                     tlm::tlm_phase& phase, sc_time& delay) {
    if (phase == tlm::BEGIN_REQ) {
      // Accept request and schedule for processing
      peq.notify(trans, delay);
      phase = tlm::END_REQ;
      return tlm::TLM_UPDATED;
    }
    if (phase == tlm::END_RESP) {
      return tlm::TLM_COMPLETED;
    }
    return tlm::TLM_ACCEPTED;
  }

  void process_transactions() {
    while (true) {
      wait(peq.get_event()); // Wait for PEQ notification

      tlm::tlm_generic_payload* trans;
      while ((trans = peq.get_next_transaction()) != nullptr) {
        // Process scheduled work here.
        wait(10, SC_NS); // Simulated processing time

        trans->set_response_status(tlm::TLM_OK_RESPONSE);
        tlm::tlm_phase bw_phase = tlm::BEGIN_RESP;
        sc_time bw_delay = SC_ZERO_TIME;

        // Call back to initiator
        socket->nb_transport_bw(*trans, bw_phase, bw_delay);
      }
    }
  }
};

SC_MODULE(NBInitiator) {
  tlm_utils::simple_initiator_socket<NBInitiator> socket{"socket"};

  SC_CTOR(NBInitiator) {
    socket.register_nb_transport_bw(this, &NBInitiator::nb_transport_bw);
    SC_THREAD(run);
  }

  void run() {
    tlm::tlm_generic_payload trans;
    sc_time delay = SC_ZERO_TIME;
    tlm::tlm_phase phase = tlm::BEGIN_REQ;
    unsigned char data = 0xFF;

    trans.set_command(tlm::TLM_WRITE_COMMAND);
    trans.set_address(0x100);
    trans.set_data_ptr(&data);
    trans.set_data_length(1);

    // Send request
    tlm::tlm_sync_enum status = socket->nb_transport_fw(trans, phase, delay);

    if (status == tlm::TLM_UPDATED && phase == tlm::END_REQ) {
        std::cout << "Target accepted request at " << sc_time_stamp() << "\n";
    }
    wait(); // Yield
  }

  tlm::tlm_sync_enum nb_transport_bw(tlm::tlm_generic_payload& trans,
                                     tlm::tlm_phase& phase, sc_time& delay) {
    if (phase == tlm::BEGIN_RESP) {
      std::cout << "Initiator received response at " << sc_time_stamp() << "\n";
      phase = tlm::END_RESP;
      return tlm::TLM_COMPLETED;
    }
    return tlm::TLM_ACCEPTED;
  }
};

int sc_main(int argc, char* argv[]) {
  NBInitiator init("init");
  NBTarget tgt("tgt");

  init.socket.bind(tgt.socket);
  sc_start(100, SC_NS);
  return 0;
}
```

## When to Avoid Non-Blocking TLM

Do not use non-blocking transport just because it feels more professional. Use it when the model needs:
- multiple outstanding transactions
- pipelined request/response behavior
- approximately timed modeling
- timing points visible to both initiator and target
- protocol-level backpressure

For simple memory-mapped platforms doing sequential fetch-decode-execute loops, blocking transport (`b_transport`) is often better and significantly faster.

## Under the Hood: The TLM Base Protocol State Machine
The non-blocking transport (`nb_transport_fw` and `nb_transport_bw`) implements a state machine using the `tlm_phase` enum.
In `src/tlm_core/tlm_2/tlm_generic_payload/tlm_phase.h`, the base protocol defines four standard phases: `BEGIN_REQ`, `END_REQ`, `BEGIN_RESP`, `END_RESP`.
When a master calls `nb_transport_fw(trans, phase, delay)`, it passes the phase by reference. The target can update the phase and return `TLM_UPDATED`, or return `TLM_ACCEPTED` indicating it will send the response later via `nb_transport_bw`. This entirely avoids SystemC `wait()` calls, achieving high simulation speeds while accurately modeling pipelined bus architectures like AXI.

## Lesson 16: Temporal Decoupling and Quantum Keepers

Canonical lesson: https://www.learn-systemc.com/tutorials/016-temporal-decoupling-and-quantum-keepers

Why fast virtual platforms let initiators run ahead of the kernel and synchronize periodically.

## How to Read This Lesson

Temporal decoupling is a performance technique for loosely timed virtual platforms. Instead of synchronizing with the SystemC kernel after every small operation, an initiator accumulates local time and synchronizes periodically.

SystemC/TLM literature often describes this as a way to improve speed interoperability for virtual platforms, as mandated by the LRM's Global Quantum concepts.

## Standard and source context

## The Cost Being Avoided

Every kernel synchronization (e.g. `wait(delay)`) has overhead because it forces a context switch out of the executing thread back to the scheduler. If an instruction set simulator synchronizes after every instruction, the platform can become unnecessarily slow.

With temporal decoupling, the initiator runs ahead locally, accumulating a time offset. The initiator still respects a global quantum (a maximum time slice). It is not allowed to run arbitrarily far ahead of the rest of the simulated system.

## Complete Quantum Keeper Example

A temporally decoupled initiator sends transactions with an annotated delay that includes its local time offset. Targets add their own delay to that reference. The initiator updates its local quantum keeper after the call.

Here is a fully compilable example of a TLM initiator utilizing a `tlm_quantumkeeper`:

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <tlm_utils/tlm_quantumkeeper.h>

using namespace sc_core;

SC_MODULE(FastTarget) {
  tlm_utils::simple_target_socket<FastTarget> socket{"socket"};
  SC_CTOR(FastTarget) {
    socket.register_b_transport(this, &FastTarget::b_transport);
  }
  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    // Target adds its processing time to the delay annotation
    delay += sc_time(10, SC_NS);
    trans.set_response_status(tlm::TLM_OK_RESPONSE);
  }
};

SC_MODULE(DecoupledInitiator) {
  tlm_utils::simple_initiator_socket<DecoupledInitiator> socket{"socket"};
  tlm_utils::tlm_quantumkeeper qk;

  SC_CTOR(DecoupledInitiator) {
    // Set global quantum (e.g., 1000 ns)
    tlm::tlm_global_quantum::instance().set(sc_time(1000, SC_NS));
    // Reset local quantum keeper
    qk.reset();

    SC_THREAD(run);
  }

  void run() {
    for (int i = 0; i < 5; ++i) {
      tlm::tlm_generic_payload trans;
      trans.set_command(tlm::TLM_READ_COMMAND);
      trans.set_address(0x0);

      // Pass local time offset instead of SC_ZERO_TIME
      sc_time local_delay = qk.get_local_time();
      socket->b_transport(trans, local_delay);

      // Update quantum keeper with the new delay computed by target
      qk.set(local_delay);

      std::cout << "Transaction " << i
                << " finished at kernel time " << sc_time_stamp()
                << " + local offset " << qk.get_local_time() << "\n";

      // If we exceeded the quantum, sync with the kernel!
      if (qk.need_sync()) {
        std::cout << "--> Syncing with kernel!\n";
        qk.sync();
      }
    }
  }
};

int sc_main(int argc, char* argv[]) {
  DecoupledInitiator init("init");
  FastTarget tgt("tgt");
  init.socket.bind(tgt.socket);

  sc_start();
  return 0;
}
```

This keeps transaction timing meaningful without forcing a kernel context switch for every operation.

## Choosing the Quantum

A larger quantum usually improves speed but reduces timing precision. A smaller quantum improves synchronization precision but increases overhead.

Choose based on the model's purpose:
- firmware bring-up: larger quantum may be fine
- interrupt-latency exploration: smaller quantum
- bus-performance exploration: validate against a more detailed model
- cycle-level questions: temporal decoupling may be the wrong abstraction

## Interrupts and Synchronization

Interrupts are the classic trap. If a CPU model runs far ahead locally, it may observe interrupts late unless the platform uses synchronization points carefully. Temporal decoupling is a controlled lie for speed. It is useful when the lie does not invalidate the engineering question.

## Under the Hood: `tlm_quantumkeeper` and Local Time
To minimize context switching, TLM initiators use a `tlm_quantumkeeper`.
Instead of calling `wait(delay)` (which halts the coroutine and returns control to the `sc_simcontext`), the initiator accumulates the delay in a local variable `m_local_time`.
The quantum keeper checks if `m_local_time` has exceeded the globally configured `m_global_quantum` (`src/tlm_core/tlm_2/tlm_quantum/tlm_global_quantum.cpp`). Only when the local time exceeds this threshold does the keeper call `wait(m_local_time)`, synchronizing the thread with the main SystemC scheduler.
This temporal decoupling can speed up instruction set simulators (ISS) by 10x-100x.

## Lesson 17: Generic Payload Extensions

Canonical lesson: https://www.learn-systemc.com/tutorials/017-generic-payload-extensions

How to attach protocol-specific metadata to TLM transactions without replacing tlm_generic_payload.

## How to Read This Lesson

`tlm_generic_payload` covers common transaction fields, but real platforms often need extra metadata: privilege level, cache attributes, security state, transaction ID, burst information, debug flags, or initiator identity.

TLM extensions let you attach that metadata without inventing a new payload class, adhering to the IEEE 1666 LRM extension mechanism.

## Standard and source context

## Extension Shape and Complete Example

An extension derives from `tlm::tlm_extension<T>`. The clone and copy functions matter because payloads can be reused, copied, or managed by memory managers.

Here is a complete, fully compilable example demonstrating how to declare, attach, and read a privilege extension across a blocking transport call.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>

using namespace sc_core;

// 1. Define the Extension
struct PrivilegeExtension : public tlm::tlm_extension<PrivilegeExtension> {
  bool privileged = false;

  tlm_extension_base* clone() const override {
    return new PrivilegeExtension(*this);
  }

  void copy_from(tlm_extension_base const& ext) override {
    privileged = static_cast<PrivilegeExtension const&>(ext).privileged;
  }
};

// 2. The Target reading the Extension
SC_MODULE(TargetDevice) {
  tlm_utils::simple_target_socket<TargetDevice> socket{"socket"};

  SC_CTOR(TargetDevice) {
    socket.register_b_transport(this, &TargetDevice::b_transport);
  }

  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    PrivilegeExtension* priv_ext = nullptr;
    trans.get_extension(priv_ext); // Attempt to retrieve the extension

    if (priv_ext && priv_ext->privileged) {
      std::cout << "[TARGET] Privileged access granted.\n";
      trans.set_response_status(tlm::TLM_OK_RESPONSE);
    } else {
      std::cout << "[TARGET] Error: Unprivileged access denied!\n";
      trans.set_response_status(tlm::TLM_COMMAND_ERROR_RESPONSE);
    }
  }
};

// 3. The Initiator attaching the Extension
SC_MODULE(CpuInitiator) {
  tlm_utils::simple_initiator_socket<CpuInitiator> socket{"socket"};

  SC_CTOR(CpuInitiator) { SC_THREAD(run); }

  void run() {
    tlm::tlm_generic_payload trans;
    sc_time delay = SC_ZERO_TIME;

    // Allocate and configure the extension
    PrivilegeExtension* priv_ext = new PrivilegeExtension();
    priv_ext->privileged = true;
    trans.set_extension(priv_ext); // Attach to transaction

    trans.set_command(tlm::TLM_READ_COMMAND);
    socket->b_transport(trans, delay);

    // Always clear extensions if managing memory manually, or if reusing payloads
    trans.clear_extension(priv_ext);
    delete priv_ext;

    wait(delay);
  }
};

int sc_main(int argc, char* argv[]) {
  CpuInitiator cpu("cpu");
  TargetDevice tgt("tgt");
  cpu.socket.bind(tgt.socket);
  sc_start();
  return 0;
}
```

For long-lived transactions or non-blocking payloads, use an ownership strategy that matches the payload lifetime. Do not attach a pointer to a stack object if the transaction may outlive the call (as is the case with non-blocking PEQ logic).

## Targets and Absent Extensions

Targets should treat absent extensions deliberately. Either define a default behavior (e.g. assume unprivileged) or report an error for protocols that require the metadata.

## Extension Hygiene

Extensions are powerful, but they can make models implicit. Keep them documented:

- Name every extension after the protocol concept it represents.
- Define who creates it, who reads it, and who owns it (memory management is key).
- State whether it is optional or required.
- Include it in transaction logs when relevant.

Good extension design lets a generic payload stay generic while preserving protocol-specific meaning.

## Exhaustive Deep Dive: IEEE 1666-2023 LRM and Accellera Source Implementation

To truly master TLM generic payload extensions, one must examine both the **IEEE 1666-2023 Standard (Section 14)** and the Accellera SystemC reference implementation located in `src/tlm_core/tlm_2/tlm_generic_payload`.

### IEEE 1666-2023 LRM: Section 14.2 and 14.3 Semantics
The LRM explicitly defines the `tlm_generic_payload` as the standard interoperability mechanism for TLM-2.0 models. However, recognizing that no single payload can capture the intricacies of every bus protocol (e.g., AMBA AXI/AHB, PCIe, OCP), **Section 14.3 (Extension Mechanism)** introduces `tlm_extension_base` and the CRTP (Curiously Recurring Template Pattern) based `tlm_extension<T>`.

**LRM Clause 14.3.1 (tlm_extension_base)** mandates that extensions must provide:
1. `virtual tlm_extension_base* clone() const = 0;`
2. `virtual void copy_from(tlm_extension_base const&) = 0;`
3. `virtual void free() { delete this; }`

The LRM strictly dictates that memory management of extensions is the responsibility of the creator, unless explicitly handed over to a memory manager. **Section 14.2.3** defines the behavior of the payload's `deep_copy_from()` method, which calls `copy_from()` on all attached extensions. If an initiator forgets to implement `clone()` or `copy_from()` correctly, deep payload copies (often used in non-blocking PEQ queues) will corrupt memory or slice the extension object.

### Accellera Source Code: `tlm_extension.h` and CRTP
If you open `src/tlm_core/tlm_2/tlm_generic_payload/tlm_extension.h`, you will find the implementation of the CRTP pattern used to assign unique IDs to extensions:

```cpp
template <typename T>
class tlm_extension : public tlm_extension_base {
public:
    virtual tlm_extension_base* clone() const = 0;
    virtual void copy_from(tlm_extension_base const &ext) = 0;
    virtual ~tlm_extension() {}
    static const unsigned int ID;
};

template <typename T>
const unsigned int tlm_extension<T>::ID = register_extension(typeid(T).name());
```

This is a beautiful piece of C++ engineering. Because `tlm_extension<T>` is a template, `T::ID` is instantiated exactly once per unique extension type `T` across the entire simulation during static initialization. The `register_extension` function (defined in `tlm_gp.cpp`) returns a monotonically increasing integer.

### Array Indexing for O(1) Access
Why go through the trouble of CRTP and static IDs? Performance.
In `src/tlm_core/tlm_2/tlm_generic_payload/tlm_gp.h`, the extensions are stored inside the payload as a flat array of pointers:

```cpp
tlm_extension_base* m_extensions[max_num_extensions];
```

When you call `trans.set_extension(ext)` or `trans.get_extension<T>()`, the Accellera kernel does **not** use `std::map`, string lookups, or slow `dynamic_cast`. It uses the statically assigned `ID` as an array index:

```cpp
template <typename T>
void get_extension(T*& ext) const {
    ext = static_cast<T*>(m_extensions[T::ID]);
}
```

This ensures that attaching or reading an extension costs nothing more than an array indexing operation and a `static_cast`â€”executing in a couple of CPU cycles. This design choice is fundamental to why TLM-2.0 loosely-timed (LT) models can simulate at speeds of hundreds of millions of transactions per second.

### Extension Memory Management and `tlm_gp::clear_extension`
A frequent source of memory leaks in TLM models occurs when extensions are attached but never deleted.

**LRM Section 14.2.3.1** specifies `clear_extension(const T*)` and `clear_extension<T>()`. In the Accellera source, `clear_extension` simply sets `m_extensions[T::ID] = 0`. It **does not** delete the extension pointer.
If an extension is attached to a generic payload that is managed by an `sc_pool` or a custom `tlm_mm_interface`, the extension will live forever unless you manually invoke `delete` or override the `free()` virtual method in conjunction with the memory manager.

When designing custom memory managers (LRM 14.2.4), developers must explicitly loop through the `m_extensions` array (or track attached extensions) and call `free()` on them when the payload's reference count reaches zero. The standard SystemC implementation provides `tlm_generic_payload::reset()`, which wipes the array but again, assumes the owner has cleaned up the actual memory.

### Sticky Extensions and `set_auto_extension`
An advanced, lesser-known feature in `tlm_gp.h` is `set_auto_extension`. Normal extensions are cleared when `reset()` is called. However, "auto extensions" or sticky extensions are preserved across payload resets. This is heavily used when an initiator creates a pool of payloads, attaches an initiator-ID extension to all of them at initialization time, and wants that extension to persist across thousands of transaction cycles without reallocating it.

Understanding this dual layerâ€”the LRM's strict behavioral contract and Accellera's highly optimized, template-driven O(1) array implementationâ€”is what separates a casual SystemC user from an expert TLM architect.

## Lesson 18: Advanced: TLM Memory Management

Canonical lesson: https://www.learn-systemc.com/tutorials/018-advanced-tlm-memory-management

Avoiding segmentation faults and memory leaks by mastering the tlm_mm_interface and payload acquire/release semantics.

## How to Read This Lesson

# Advanced TLM Pitfalls: Memory Management

If you browse the Accellera SystemC forums, the most common cause of advanced simulation crashes (Segmentation Faults) is the mismanagement of the **TLM 2.0 Generic Payload (`tlm_generic_payload`)**.

Unlike a simple `int` or `bool`, a `tlm_generic_payload` is a massive object. It contains pointers to data buffers, byte enable arrays, extension arrays, and response statuses. Allocating and deallocating this object using standard C++ `new` and `delete` millions of times per second (e.g., for every memory read/write transaction) will bottleneck your simulation speed completely.

To solve this, the TLM 2.0 LRM dictates the use of a **Memory Manager**. Let's look at the **Accellera TLM kernel source code** to see why this is critical.

## Standard and source context

## The Problem with `new` and `delete`

A naive implementation of a TLM initiator might look like this:

```cpp
// BAD CODE - Do not do this!
void send_transaction() {
    tlm::tlm_generic_payload* trans = new tlm::tlm_generic_payload();
    // ... setup trans ...
    socket->b_transport(*trans, delay);
    delete trans; // Highly expensive and prone to dangling pointer crashes!
}
```

This is incredibly slow. Instead, the IEEE 1666 LRM requires maintaining a pool of payload objects and reusing them.

## The Memory Manager (`tlm_mm_interface`)

TLM defines the `tlm::tlm_mm_interface`, providing two core virtual methods:
- `allocate()`: Returns an unused payload from the pool.
- `free(tlm_generic_payload* trans)`: Returns a payload back to the pool.

### Complete Memory Manager Implementation Example

You must attach a memory manager to your payload upon creation. While `tlm_utils` provides some basic managers (`tlm_utils::simple_peq_with_cb`), writing a custom compliant one teaches you the exact standard mechanics.

```cpp
#include <systemc>
#include <tlm>
#include <vector>

// 1. A Custom IEEE 1666 Compliant Memory Manager
class CustomMemoryManager : public tlm::tlm_mm_interface {
    std::vector<tlm::tlm_generic_payload*> free_list;
public:
    tlm::tlm_generic_payload* allocate() {
        if (free_list.empty()) {
            // Allocate a new payload and bind it to THIS memory manager
            return new tlm::tlm_generic_payload(this);
        } else {
            tlm::tlm_generic_payload* trans = free_list.back();
            free_list.pop_back();
            return trans;
        }
    }

    void free(tlm::tlm_generic_payload* trans) override {
        trans->reset(); // Critical: Reset fields before returning to pool
        free_list.push_back(trans);
    }
};

SC_MODULE(InitiatorMM_Demo) {
    CustomMemoryManager mm;

    SC_CTOR(InitiatorMM_Demo) {
        SC_THREAD(run_transactions);
    }

    void run_transactions() {
        // First transaction (allocates new)
        tlm::tlm_generic_payload* trans1 = mm.allocate();
        trans1->acquire(); // Rule: Acquire before use

        std::cout << "Transaction 1 Acquired." << std::endl;

        // ... Send through socket (omitted for brevity) ...

        trans1->release(); // Drops ref count to 0, calls mm.free() automatically
        std::cout << "Transaction 1 Released." << std::endl;

        // Second transaction (reuses the same memory block from the pool!)
        tlm::tlm_generic_payload* trans2 = mm.allocate();
        trans2->acquire();
        std::cout << "Transaction 2 Acquired (Reused memory block)." << std::endl;
        trans2->release();
    }
};

int sc_main(int argc, char* argv[]) {
    InitiatorMM_Demo initiator("initiator");
    sc_core::sc_start();
    return 0;
}
```

## The Golden Rules of `acquire()` and `release()`

When a payload travels through an SoC, it might pass through multiple routers, caches, and targets. How does the initiator know when it is safe to `free()` the payload back to the pool? What if a target stored a pointer to the payload in a queue to process it asynchronously via a Payload Event Queue (PEQ)?

This is where `acquire()` and `release()` come in. They implement an atomic **Reference Counting** mechanism embedded directly in the `tlm_generic_payload` base class.

**Under the Hood (Accellera TLM source code):**
The `tlm_generic_payload` contains an integer `m_ref_count` and a pointer `tlm_mm_interface* m_mm`.
1. **Rule 1: Acquiring.** When you call `trans->acquire()`, the source code simply executes `m_ref_count++`. If any component (router, target, observer) intends to keep a pointer to the payload *after* the current function call (e.g., `b_transport` or `nb_transport`) returns, it **MUST** call `trans->acquire()`.
2. **Rule 2: Releasing.** Once that component is done with the payload, it **MUST** call `trans->release()`. The source code executes `m_ref_count--`.
3. **Rule 3: The Auto-Free Hook.** Inside the `release()` implementation, there is a check: `if (m_ref_count == 0 && m_mm != 0) { m_mm->free(this); }`. This automatically returns the payload to your memory manager pool.
4. **Rule 4: Extensions cleanup:** During `m_mm->free()`, you must call `trans->reset()`. In the Accellera kernel, `reset()` iterates over the `m_extensions` vector. If an extension has a memory manager attached, it frees it. If it doesn't, it might leave dangling memory unless managed correctly by `free_all_extensions()`.

Failure to follow these rules will result in catastrophic memory leaks (forgetting to release) or impossible-to-debug segmentation faults (releasing too early while a target is still reading data). Always use a Memory Manager in production code.

## Source-reading checkpoint

For payload reuse, inspect `tlm_core` around `tlm_generic_payload`, extension storage, and memory-manager callbacks. The implementation shows why reset discipline matters.

## Lesson 19: TLM Performance: DMI, Quantum Tuning, and Payload Discipline

Canonical lesson: https://www.learn-systemc.com/tutorials/019-tlm-performance-dmi-quantum-tuning-and-payload-dis

A senior-level guide to making TLM virtual platforms fast without breaking timing, ordering, or debug behavior.

## How to Read This Lesson

# TLM Performance: DMI, Quantum Tuning, and Payload Discipline

TLM performance is not one trick. It is a set of disciplined choices: transport style, payload reuse, direct memory interface, temporal decoupling, report cost, and how much timing fidelity the use case really needs.

## Standard and source context

## DMI and Payload Reuse Example

DMI (Direct Memory Interface) lets an initiator bypass repeated socket calls for memory-like regions. It is most valuable for RAM and ROM. Generic payloads should also be reused to avoid constant heap allocation.

Here is a full compilable example demonstrating DMI and payload reuse:

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>

using namespace sc_core;

SC_MODULE(FastMemory) {
  tlm_utils::simple_target_socket<FastMemory> socket{"socket"};
  unsigned char memory[1024];

  SC_CTOR(FastMemory) {
    socket.register_b_transport(this, &FastMemory::b_transport);
    socket.register_get_direct_mem_ptr(this, &FastMemory::get_direct_mem_ptr);
  }

  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    // Normal slow-path transport
    trans.set_response_status(tlm::TLM_OK_RESPONSE);
    trans.set_dmi_allowed(true); // Hint to initiator that DMI is available
  }

  bool get_direct_mem_ptr(tlm::tlm_generic_payload& trans, tlm::tlm_dmi& dmi_data) {
    // Grant DMI access to the entire 1KB memory
    dmi_data.allow_read_write();
    dmi_data.set_dmi_ptr(memory);
    dmi_data.set_start_address(0);
    dmi_data.set_end_address(1023);
    dmi_data.set_read_latency(SC_ZERO_TIME);
    dmi_data.set_write_latency(SC_ZERO_TIME);
    return true;
  }
};

SC_MODULE(OptimizedInitiator) {
  tlm_utils::simple_initiator_socket<OptimizedInitiator> socket{"socket"};
  tlm::tlm_generic_payload reused_payload; // Payload reuse

  unsigned char* dmi_ptr = nullptr;
  uint64_t dmi_start = 0, dmi_end = 0;
  bool dmi_valid = false;

  SC_CTOR(OptimizedInitiator) { SC_THREAD(run); }

  void run() {
    uint64_t addr = 0x10;

    // First attempt: try DMI directly
    if (dmi_valid && addr >= dmi_start && addr <= dmi_end) {
      dmi_ptr[addr - dmi_start] = 0xAA;
      return;
    }

    // Slow path: configure reused payload
    unsigned char data = 0xAA;
    reused_payload.set_command(tlm::TLM_WRITE_COMMAND);
    reused_payload.set_address(addr);
    reused_payload.set_data_ptr(&data);
    reused_payload.set_data_length(1);
    reused_payload.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

    sc_time delay = SC_ZERO_TIME;
    socket->b_transport(reused_payload, delay);

    // Check if target hinted at DMI
    if (reused_payload.is_dmi_allowed()) {
      tlm::tlm_dmi dmi_data;
      if (socket->get_direct_mem_ptr(reused_payload, dmi_data)) {
        dmi_valid = true;
        dmi_ptr = dmi_data.get_dmi_ptr();
        dmi_start = dmi_data.get_start_address();
        dmi_end = dmi_data.get_end_address();
        SC_REPORT_INFO(name(), "DMI successfully established!");
      }
    }
  }
};

int sc_main(int argc, char* argv[]) {
  OptimizedInitiator init("init");
  FastMemory mem("mem");
  init.socket.bind(mem.socket);
  sc_start();
  return 0;
}
```

## DMI Safety

Targets should grant DMI only when direct access is safe (e.g. memory is contiguous, invalidation is implemented). Routers must translate DMI ranges. If a target grants a local address window, the router returns the corresponding system address window.

## Extensions and Report Cost

Project policy should define who owns extensions and when they are cleared.
Avoid format strings in hot paths unless tracing is enabled. Do not format complex messages that will never be displayed.

## Expert Checklist

A performant TLM VP should:

- use blocking transport for simple memory-mapped software access
- use DMI for RAM/ROM
- use temporal decoupling only with clear synchronization policy
- reuse payloads in hot initiators
- avoid dynamic allocation in per-transaction paths
- set response status every time

## Exhaustive Deep Dive: IEEE 1666-2023 LRM and Accellera TLM Source Implementation

Performance in TLM-2.0 is not magic; it is a meticulously architected set of bypass mechanisms and synchronization policies codified in the **IEEE 1666-2023 LRM**. The two most critical optimizations are the Direct Memory Interface (DMI) and Temporal Decoupling. Let's trace both the LRM mandates and their exact source implementations.

### DMI: LRM Section 11 and `tlm_dmi`
The **Direct Memory Interface (LRM Section 11)** exists because function calls through sockets for every byte of memory access are too slow for instruction-set simulators (ISS) booting Linux.

**LRM Clause 11.2** defines the `tlm_dmi` class. This is not a transaction payload; it is a metadata container granting a direct pointer to a contiguous block of memory. When you inspect `src/tlm_core/tlm_2/tlm_dmi/tlm_dmi.h` in the Accellera source, the class boils down to:
```cpp
class tlm_dmi {
    unsigned char* m_dmi_ptr;
    sc_dt::uint64  m_start_address;
    sc_dt::uint64  m_end_address;
    dmi_access_e   m_granted_access; // TLM_DMI_READ, TLM_DMI_WRITE, etc.
    sc_core::sc_time m_read_latency;
    sc_core::sc_time m_write_latency;
    // ...
};
```
When an initiator calls `get_direct_mem_ptr`, it is asking the target to populate this object. If successful, the initiator extracts `m_dmi_ptr`. From that moment on, the initiator completely bypasses SystemC:
```cpp
// Ultimate fast-path: Pure C++ array access
dmi_ptr[address - dmi_start_address] = data;
```
This bypasses `b_transport`, the scheduler, payload creation, and sockets.

**Invalidation (LRM 11.2.5):** If a target needs to remap memory or change access rights, it **must** call `invalidate_direct_mem_ptr` on its socket. This propogates backward to all initiators, forcing them to flush their cached `tlm_dmi` pointers and fall back to `b_transport`.

### Temporal Decoupling and the Quantum Keeper (LRM Section 12)
If an initiator uses DMI to execute 1,000 instructions, it takes 0 delta cycles in SystemC. If it calls `wait()` after every instruction, the SystemC scheduler becomes the bottleneck. **Temporal Decoupling (LRM 12.1)** solves this.

Instead of yielding to the kernel, the initiator accumulates a local "time offset". It runs ahead of the official simulation time `sc_time_stamp()`.
The LRM introduces the **Global Quantum (LRM 12.2.3)**, which represents the maximum time an initiator is allowed to run ahead before it *must* yield to allow other threads to catch up.

In `src/tlm_core/tlm_2/tlm_quantum/tlm_global_quantum.h`, the global quantum is implemented as a singleton:
```cpp
class tlm_global_quantum {
public:
    static tlm_global_quantum& instance();
    void set( const sc_core::sc_time& t );
    const sc_core::sc_time& get() const;
    // ...
};
```

To manage local offsets safely, the LRM recommends the **Quantum Keeper (LRM 12.2.4)**, implemented in `src/tlm_core/tlm_2/tlm_quantum/tlm_quantumkeeper.h`. An initiator uses it like this:
```cpp
tlm_utils::tlm_quantumkeeper m_qk;

void run() {
    while(true) {
        // Execute instruction
        m_qk.inc(sc_time(10, SC_NS));

        // If local time exceeds the global quantum, yield!
        if (m_qk.need_sync()) {
            m_qk.sync(); // Calls wait(m_qk.get_local_time())
        }
    }
}
```
Inside `tlm_quantumkeeper::sync()`, the keeper calls SystemC's `wait(m_local_time)` and then resets the local time offset to zero. This dramatically reduces the number of context switches. Instead of switching threads 10,000 times for 10,000 clock cycles, the threads switch only once per quantum (e.g., every 10,000 cycles).

### Payload Allocation Discipline (LRM Section 14.2.4)
Even without DMI, memory allocation destroys performance. Profiling a naive TLM model often reveals that `new tlm_generic_payload` consumes 80% of execution time.
The Accellera reference implementation provides `tlm_utils::peq_with_cb_and_phase` and `tlm_mm_interface` to help, but the golden rule for blocking transport (`b_transport`) is simple: **Allocate once, reuse infinitely.**

An initiator should instantiate a single `tlm_generic_payload` as a class member, configure only the fields that change (command, address, data pointer), and send it.
For non-blocking transport, where payloads fly concurrently, you must implement a Memory Manager (`tlm_mm_interface`). The core source `src/tlm_core/tlm_2/tlm_generic_payload/tlm_gp.cpp` shows that `acquire()` and `release()` literally just increment and decrement an internal `m_ref_count`. When it hits zero, the payload calls `m_mm->free(this)` instead of `delete`, allowing the memory manager to return the payload to an `sc_pool` for reuse.

Mastering DMI, the Quantum Keeper, and Payload memory management forms the triad of TLM virtual platform performance.

## Lesson 20: TLM Base Protocol Rules and Checker Patterns

Canonical lesson: https://www.learn-systemc.com/tutorials/020-tlm-base-protocol-rules-and-checker-patterns

How to review blocking and non-blocking TLM transactions for legal phases, response status, ownership, DMI, and debug behavior.

## How to Read This Lesson

TLM lets you move fast because a transaction is just a C++ object passed through an interface. The cost of that speed is discipline: the protocol rules live in the payload fields, phases, timing annotations, and ownership conventions.

## Standard and source context

Use `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for TLM-2.0 base protocol behavior. In source, inspect `Accellera SystemC GitHub repository`, `tlm_2_interfaces`, `tlm_sockets`, and `tlm_quantum`.

## Blocking Transport Review

In blocking transport, the initiator calls:

```cpp
socket->b_transport(trans, delay);
```

The target must treat the payload as an in-progress transaction and set a meaningful response status before returning.

Review these fields:

- command: read, write, or ignore
- address: target-visible address
- data pointer: valid storage for the access
- data length: number of bytes requested
- streaming width: wrapping semantics for streaming transfers
- byte enable pointer: null means all bytes enabled
- response status: must not remain incomplete after target completion
- DMI allowed: target may hint that DMI is worth requesting

## Non-Blocking Base Protocol

The base protocol uses four canonical phases:

```text
BEGIN_REQ -> END_REQ -> BEGIN_RESP -> END_RESP
```

The tricky part is that `nb_transport_fw` and `nb_transport_bw` pass the phase by reference and return a status:

- `TLM_ACCEPTED`: I accepted this phase and will respond later.
- `TLM_UPDATED`: I changed the phase and delay before returning.
- `TLM_COMPLETED`: transaction is complete now.

That gives the model flexibility, but it also creates review burden. A phase checker should verify that transitions are legal for the protocol style your project uses.

## Checker Pattern

A simple checker can live beside an interconnect:

```cpp
struct TxnState {
  bool request_seen = false;
  bool response_seen = false;
};

void check_phase(TxnState& s, tlm::tlm_phase phase) {
  if (phase == tlm::BEGIN_REQ) {
    if (s.request_seen) {
      SC_REPORT_ERROR("TLM_CHECK", "duplicate BEGIN_REQ");
    }
    s.request_seen = true;
  }

  if (phase == tlm::BEGIN_RESP) {
    if (!s.request_seen) {
      SC_REPORT_ERROR("TLM_CHECK", "response before request");
    }
    s.response_seen = true;
  }
}
```

Real checkers also track transaction identity, outstanding request limits, exclusion rules, and protocol-specific extensions.

## How the Source Makes This Work

The Accellera TLM implementation does not magically know your bus protocol. It defines the base interfaces, payload, phases, sockets, and helper classes. Your model or protocol checker must enforce higher-level rules such as AXI ordering, ID handling, burst legality, and exclusive access semantics.

That distinction is important: TLM-2.0 gives you the transport framework. It does not make every transaction valid for your architecture.

## DMI and Debug Review

DMI is a promise that direct pointer access is safe for an address range. If a target grants DMI, it must invalidate that pointer when the range becomes unsafe.

Debug transport is a promise that the access is for inspection and should not change modeled state in surprising ways. A debug read to a FIFO should not consume data. A debug write should be documented very carefully.

## Review Checklist

- Does every completed transaction set a response status?
- Are data pointers and lengths valid for the access?
- Are non-blocking phases legal for the chosen protocol?
- Is payload ownership clear?
- Are extensions copied, cloned, and released correctly?
- Is DMI invalidated when memory maps or permissions change?
- Is `transport_dbg` side-effect free?

## Lesson 21: TLM API, Socket, and Utility Field Guide

Canonical lesson: https://www.learn-systemc.com/tutorials/132-tlm-api-socket-and-utility-field-guide

A focused guide to TLM-2.0 interfaces, sockets, payload utilities, endian helpers, and source implementation symbols.

## How to Read This Lesson

TLM names can look like a wall of templates. Read them by role: transport interface, socket wrapper, payload object, analysis channel, or utility helper. Once you can classify the name, the C++ template shape becomes much less intimidating.

## Standard and source context

Use the TLM clauses in `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for portable behavior. Use `.codex-src/systemc/src/tlm_core` and `.codex-src/systemc/src/tlm_utils` for implementation reading.

## Transport Interfaces

`tlm_blocking_transport_if`, `tlm_fw_transport_if`, `tlm_bw_transport_if`, `tlm_fw_direct_mem_if`, `tlm_bw_direct_mem_if`, `tlm_fw_nonblocking_transport_if`, and `tlm_bw_nonblocking_transport_if` define who can call whom. The naming is literal: forward path goes initiator to target, backward path goes target to initiator.

`tlm_transport_dbg_if`, `tlm_transport_channel`, `tlm_core_ifs`, and plain "transports" vocabulary sit beside the same idea: a transaction-level model is primarily a set of typed transport calls, not signal toggles.

`b_transport` is the blocking transport call. It is easiest to reason about because one call represents one transaction and the delay argument is updated as the call travels.

`nb_transport_fw` and `nb_transport_bw` are non-blocking transport calls. They carry a `tlm_phase` and return a `tlm_sync_enum`, so you review them as a state machine, not as a normal function call.

## Socket Families

`tlm_base_socket_if`, `tlm_base_initiator_socket`, `tlm_base_initiator_socket_b`, `tlm_base_initiator_socket_export`, `tlm_base_target_socket`, `tlm_base_target_socket_b`, `tlm_base_target_socket_port`, `tlm_initiator_socket`, and `tlm_target_socket` are the socket architecture.

The practical split is simple. Base socket types hold the generic binding machinery. Concrete initiator and target sockets are what models normally instantiate. If you are debugging a failed bind, follow the socket down to the port/export/interface layers rather than starting with the payload.

`tlm_base_protocol_types`, `tlm_base_protocol`, `tlm_payload_type`, `tlm_phase_type`, and `tlm_socket_category` are template vocabulary that lets the same socket framework support a protocol-specific payload and phase set.

The plural documentation names `tlm_initiator_sockets`, `tlm_target_sockets`, and `tlm_ports` usually indicate documentation groups rather than classes you instantiate directly. In source reading, they still help you locate the socket and port families.

Implementation names such as `tlm_initiator_socket_base`, `tlm_initiator_socket_b`, `tlm_target_socket_base`, `tlm_target_socket_b`, `tlm_slave_imp`, and `tlm_nonblocking_port` are adapter and base-class pieces under the friendly socket API.

## Payload, DMI, and Extensions

`tlm_generic_payload` is the standard transaction object. It carries command, address, data pointer, byte enable pointer, streaming width, response status, and extensions. Every target should leave the response status in a meaningful state.

`tlm_response_status` tells the initiator how the target handled the transaction. `tlm_gp_option` tells how much of the generic-payload protocol the transaction is using. Review both when a model mixes fast approximate paths with stricter protocol checking.

`tlm_dmi`, `get_direct_mem_ptr`, and `transport_dbg` are optional fast paths. They are not replacements for protocol correctness. Treat them as negotiated capabilities: if the target cannot guarantee the requested access, it must say no.

`tlm_extension`, `tlm_extension_registry`, and custom extension classes let you carry model-specific metadata without changing the base payload. Good extensions are documented, cloned correctly, and owned clearly.

## Analysis and TLM-1 Compatibility Names

Names such as `tlm_analysis_if`, `tlm_analysis_fifo`, `tlm_write_if`, `tlm_fifo_put_if`, `tlm_fifo_get_if`, `tlm_blocking_get_if`, `tlm_blocking_peek_if`, `tlm_blocking_get_peek_if`, `tlm_nonblocking_put_if`, `tlm_nonblocking_get_if`, `tlm_nonblocking_peek_if`, `tlm_put_if`, `tlm_get_peek_if`, `tlm_transport_if`, `tlm_slave_if`, `tlm_channels`, and `tlm_1_interfaces` mostly live around analysis and older TLM communication styles.

Also recognize `tlm_get_if`, `tlm_peek_if`, `tlm_nonblocking_get_peek_if`, `tlm_fifo_debug_if`, `tlm_delayed_write_if`, `tlm_analysis_triple`, `tlm_bool`, `tlm_req_rsp_channels`, `tlm_master_if`, `tlm_slave_to_transport`, `tlm_transport_to_master`, `tlm_master_slave_ifs`, `tlm_blocking_master_if`, `tlm_blocking_slave_if`, `tlm_nonblocking_master_if`, `tlm_nonblocking_slave_if`, `tlm_nonblocking_put_port`, `tlm_nonblocking_get_port`, and `tlm_nonblocking_peek_port`. These are mostly adapter, FIFO, analysis, and legacy channel vocabulary.

FIFO-specific implementation names include `tlm_delayed_analysis_if`, `tlm_fifo_ifs`, `tlm_fifo_config_size_if`, `tlm_fifo_peek`, `tlm_fifo_put_get`, and `tlm_fifo_resize`. Read these as channel internals rather than as the preferred public TLM-2.0 VP style.

In modern TLM-2.0 virtual-platform modeling, you usually see `tlm_generic_payload` and sockets first. The TLM-1 and analysis names still matter for scoreboards, monitors, trace collection, adapters, and legacy models.

## Endianness Helpers

`tlm_endianness`, `tlm_endian_context`, `tlm_endian_context_pool`, `tlm_from_hostendian`, `tlm_from_hostendian_aligned`, `tlm_from_hostendian_generic`, `tlm_from_hostendian_single`, `tlm_from_hostendian_word`, and related helpers exist because payload byte lanes are not the same thing as the host CPU's native layout.

The reverse helpers `tlm_to_hostendian_generic`, `tlm_to_hostendian_word`, `tlm_to_hostendian_aligned`, and `tlm_to_hostendian_single` show the same concern in the other direction. If a target converts data for host-side algorithm code, the model should document when the payload is in bus order and when it is in host order.

`tlm_endian_conv`, `tlm_data`, and `tlm_version_string_2` are lower-level helper names you may see while following conversion or version-source code.

If your platform is memory-mapped and firmware-visible, be explicit about endian assumptions. A model that passes tests on one host but changes behavior on another host is not a portable TLM model.

## Event and Phase Utilities

`tlm_phase`, `tlm_phase_registry`, `tlm_event_finder_t`, `tlm_sync_enum`, `tlm_tag`, `tlm_array`, `tlm_put_get_imp`, and payload event queue utilities are glue around protocol state and callback scheduling.

`tlm_phase_enum` and `tlm_phase_name_arg` are phase-support names. `tlm_adapters`, `tlm_helpers`, and `tlm_master_imp` show up in source areas that connect old and new communication styles.

## Version Names

`tlm_version`, `tlm_version_major`, `tlm_version_minor`, `tlm_version_patch`, `tlm_version_originator`, `tlm_version_prerelease`, `tlm_version_release_date`, `tlm_version_string`, `tlm_release`, `tlm_is_prerelease`, `tlm_copyright`, and `tlm_copyright_string` are version and distribution metadata. They do not change transaction behavior, but they are useful when diagnosing mismatched library installations.

The review question is: can you draw the phase transition for every non-blocking transaction? If not, the model may be relying on callback ordering by accident.

## Senior Review Checklist

- Are sockets bound in the intended direction?
- Does every target set response status?
- Are DMI and debug transport optional and safe?
- Are byte enables, streaming width, and endian behavior tested?
- Are extensions cloned, cleared, and documented?
- Is temporal decoupling visible in the code rather than hidden in a helper?

## Lesson 22: SystemC Source Map & LRM Alignment

Canonical lesson: https://www.learn-systemc.com/tutorials/021-systemc-source-map-and-lrm-alignment

A guided map of the official source tree: kernel, communication, datatypes, TLM, and utilities.

## How to Read This Lesson

This is a source-reading lesson. We will use the Accellera implementation as a microscope, while keeping the LRM as the portability contract.

# SystemC Source Map & LRM Alignment

The official SystemC implementation is worth reading because it explains the API design. You do not need to read it linearly. Read it as a map of responsibilities, aligned directly with the IEEE 1666 Language Reference Manual (LRM).

The public repository is [accellera-official/systemc](https://github.com/accellera-official/systemc). Directory names can move across releases, but the conceptual areas remain stable. Understanding how these conceptual areas map to the C++ implementation is the key to deep SystemC mastery.

## Standard and source context

## 1. Kernel (LRM Section 4: Elaboration and Simulation)

The kernel area contains the scheduler, simulation context, process classes, events, time, reports, and object hierarchy. Names you will encounter include:

- `sc_simcontext`: the central simulation context. It holds the event queues and Delta Cycle loop.
- `sc_object`: the base class for hierarchical objects.
- `sc_module`: structural hierarchy and process registration support.
- `sc_event`: event notification and wait support.
- `sc_process_b`, `sc_method_process`, `sc_thread_process`: implementation of methods and coroutines.

Read this area when you want to understand `sc_start()`, delta cycles, process readiness, and why `wait()` is legal in an `SC_THREAD` but throws an exception in an `SC_METHOD`.

## 2. Communication (LRM Section 6 & 7: Channels and Interfaces)

The communication area contains ports, exports, interfaces, primitive channels, signals, FIFOs, mutexes, semaphores, and clocks.

- `sc_interface`: Pure virtual base class.
- `sc_port`: The mechanism for a module to call an interface.
- `sc_signal`: The fundamental channel implementing request-update semantics.

This is where the object model becomes a network. Ports do not merely hold C++ pointers. They participate in binding policy, interface checking, and simulation setup.

## 3. Datatypes (LRM Section 7: Data Types)

SystemC includes hardware-friendly datatypes such as:
- Arbitrary precision integers (`sc_bigint`, `sc_biguint`)
- Bit vectors (`sc_bv`)
- Logic vectors (`sc_lv`, `sc_logic`)
- Fixed-point types (`sc_fixed`)

These types exist because plain C++ integer behavior cannot model hardware realities like four-state logic ('0', '1', 'Z', 'X') or precise bit-width truncation.

## 4. TLM (LRM Section 10-16: TLM-2.0)

The TLM area includes generic payloads, phases, sockets, target and initiator interfaces, and utility helpers. Sockets are not a separate universe. They package standard SystemC ports, exports, callbacks, and interfaces into a highly optimized bus-modeling form.

## Bringing it All Together: Complete Example

To illustrate how these four main quadrants of the SystemC source map (Kernel, Communication, Datatypes, TLM) synthesize into a single LRM-compliant model, here is a complete executable example.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <iostream>

using namespace sc_core;
using namespace sc_dt; // Datatypes

// ---------------------------------------------------------
// A module demonstrating Kernel processes, Communication
// channels, Datatypes, and TLM-2.0 Sockets.
// ---------------------------------------------------------
SC_MODULE(SourceMapDemo) {
  // --- COMMUNICATION ---
  sc_in<bool> clk{"clk"};

  // --- COMMUNICATION & DATATYPES ---
  // A primitive channel wrapping a 4-bit unsigned integer
  sc_signal<sc_uint<4>> counter{"counter"};

  // --- TLM ---
  tlm_utils::simple_initiator_socket<SourceMapDemo> init_socket{"init_socket"};

  SC_CTOR(SourceMapDemo) {
    // --- KERNEL ---
    // Registering a clocked thread with the simulation context
    SC_CTHREAD(clocked_thread, clk.pos());
  }

  void clocked_thread() {
    // Initialization
    counter.write(0);

    while (true) {
      // Kernel: Yield execution back to the scheduler
      wait();

      // Datatypes: Read, manipulate, and write the 4-bit type
      sc_uint<4> val = counter.read() + 1;
      counter.write(val);

      std::cout << "[Demo] Clock ticked. Counter: " << val << "\n";

      // TLM: Generate a transaction when the counter overflows (4-bit overflow)
      if (val == 0) {
        std::cout << "[Demo] Counter overflowed! Sending TLM transaction.\n";

        tlm::tlm_generic_payload trans;
        sc_time delay = SC_ZERO_TIME;

        trans.set_command(tlm::TLM_WRITE_COMMAND);
        trans.set_address(0x100);
        trans.set_data_length(4);
        trans.set_streaming_width(4);
        trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

        // Blocking transport call
        init_socket->b_transport(trans, delay);

        if (trans.is_response_ok()) {
            std::cout << "[Demo] TLM transaction completed successfully.\n";
        }
      }
    }
  }
};

// Target to satisfy the TLM binding
SC_MODULE(DummyTarget) {
  tlm_utils::simple_target_socket<DummyTarget> target_socket{"target_socket"};

  SC_CTOR(DummyTarget) {
    target_socket.register_b_transport(this, &DummyTarget::b_transport);
  }

  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    std::cout << "  -> [Target] Received transaction at address 0x"
              << std::hex << trans.get_address() << "\n";
    trans.set_response_status(tlm::TLM_OK_RESPONSE);
  }
};

// Top Level
int sc_main(int argc, char* argv[]) {
  // Kernel: Clock generation
  sc_clock clk("clk", 10, SC_NS);

  SourceMapDemo demo("demo");
  DummyTarget tgt("tgt");

  // Communication: Structural binding during Elaboration Phase
  demo.clk(clk);
  demo.init_socket.bind(tgt.target_socket);

  std::cout << "Starting Simulation...\n";
  // Kernel: Enter Simulation Phase
  sc_start(170, SC_NS);
  std::cout << "Simulation Complete.\n";

  return 0;
}
```

### Explanation of the Execution

When you run this simulation, the `sc_clock` drives the `SC_CTHREAD`. The `sc_uint<4>` datatype safely rolls over from 15 to 0. When it hits 0, it triggers the TLM-2.0 socket, bridging the cycle-accurate RTL abstraction with the Loosely Timed VP abstraction.

By understanding how these source map quadrants interact, you can read any SystemC codebase with confidence.

## Under the Hood: The Accellera Kernel Directory Map
When debugging a SystemC error, knowing the Accellera source tree is your superpower:
- **`src/sysc/kernel/sc_simcontext.cpp`**: Contains the `crunch()` function, the beating heart of the SystemC scheduler.
- **`src/sysc/kernel/sc_process.cpp`**: Manages process state machines (runnable, waiting, dead).
- **`src/sysc/qt/`**: The QuickThreads coroutine library used for `SC_THREAD` context switching.
- **`src/sysc/communication/sc_signal.cpp`**: The `sc_signal` template logic and `sc_prim_channel` update mechanisms.
- **`src/sysc/datatypes/int/`**: Implementations of bit-accurate integers.
- **`src/tlm_core/tlm_2/tlm_sockets/`**: Implementation of `tlm_initiator_socket` and `tlm_target_socket`, which are essentially convenience wrappers combining an `sc_port` and an `sc_export`.

## Lesson 23: Simulation Kernel Deep Dive

Canonical lesson: https://www.learn-systemc.com/tutorials/022-simulation-kernel-deep-dive

How evaluate-update-delta scheduling makes concurrent C++ models behave like hardware.

## How to Read This Lesson

This is a source-reading lesson. We will use the Accellera implementation as a microscope, while keeping the LRM as the portability contract.

The SystemC scheduler is a discrete-event simulation kernel. Its job is to decide which process runs, when updates become visible, which events wake new work, and when simulation time can advance. This is strictly defined by the IEEE 1666 Language Reference Manual (LRM), primarily in Section 4 on Elaboration and Simulation Semantics.

## Standard and source context

## The Shape of a Simulation Step

According to the LRM, the simulation semantics are broken down into distinct phases: Evaluation, Update, Delta Notification, and Time Advance. We can observe these phases in action by writing a complete model that tracks delta cycles.

```cpp
#include <systemc>
#include <iostream>

using namespace sc_core;

SC_MODULE(DeltaTracker) {
  sc_signal<bool> sig_a{"sig_a"};
  sc_signal<bool> sig_b{"sig_b"};

  SC_CTOR(DeltaTracker) {
    SC_THREAD(stimulus);
    SC_METHOD(monitor_a);
    sensitive << sig_a;
    dont_initialize();

    SC_METHOD(monitor_b);
    sensitive << sig_b;
    dont_initialize();
  }

  void stimulus() {
    std::cout << "[Time: " << sc_time_stamp() << ", Delta: " << sc_delta_count() << "] Stimulus: writing sig_a=1\n";
    sig_a.write(true);

    wait(SC_ZERO_TIME); // Advance one delta cycle

    std::cout << "[Time: " << sc_time_stamp() << ", Delta: " << sc_delta_count() << "] Stimulus: writing sig_b=1\n";
    sig_b.write(true);

    wait(10, SC_NS); // Advance time

    std::cout << "[Time: " << sc_time_stamp() << ", Delta: " << sc_delta_count() << "] Stimulus: done\n";
  }

  void monitor_a() {
    std::cout << "[Time: " << sc_time_stamp() << ", Delta: " << sc_delta_count() << "] monitor_a triggered: sig_a=" << sig_a.read() << "\n";
  }

  void monitor_b() {
    std::cout << "[Time: " << sc_time_stamp() << ", Delta: " << sc_delta_count() << "] monitor_b triggered: sig_b=" << sig_b.read() << "\n";
  }
};

int sc_main(int argc, char* argv[]) {
  DeltaTracker tracker("tracker");
  sc_start();
  return 0;
}
```

## Evaluate Phase

During the evaluation phase, the kernel selects a ready process and resumes its execution. A ready `SC_METHOD` runs to completion. A ready `SC_THREAD` (or `SC_CTHREAD`) resumes until it calls `wait()` or returns. The LRM explicitly states that the order of process execution during the evaluation phase is non-deterministic. Your models must not rely on the order in which processes execute within the same delta cycle.

Processes can write to signals, notify events, call TLM transport functions, and update ordinary C++ state. But primitive channel updates (like writes to `sc_signal`) are deferred.

## Update Phase

After all ready processes have been evaluated (the set of runnable processes is empty), the kernel enters the update phase. Every primitive channel that had `request_update()` called during the evaluate phase now has its `update()` function executed.

For `sc_signal`, this is when the new value actually becomes the current value. By deferring the update, SystemC guarantees that all processes reading the signal during the evaluate phase read the old value, regardless of process execution order. This avoids race conditions and mirrors the behavior of hardware registers.

## Delta Notification Phase

During the update phase, or directly from processes using zero-delay notification (`notify(SC_ZERO_TIME)`), events may be scheduled for the next delta cycle.

If there are pending delta notifications, the kernel advances to the next delta cycle (incrementing `sc_delta_count()`) while keeping the simulation time constant, and moves those notified processes back to the runnable set. It then loops back to the Evaluate phase.

This gives SystemC a way to settle combinational behavior without advancing time.

## Time Advance Phase

When the runnable set is empty and there are no more primitive channel updates or delta notifications, the current time step is fully settled. The scheduler then looks for the next earliest timed event (e.g., a process waiting for `10 ns` or a delayed notification). Simulation time jumps directly to that time, the relevant events are triggered, processes become ready, and the loop starts again at the Evaluate phase.

## Why wait() Needs Process State

`SC_THREAD` can suspend and resume. That means the implementation needs a process control mechanism (such as coroutines, fibers, or user-level threads like QuickThreads in the reference implementation) that preserves the call stack and execution state across waits. `wait()` returns control to the kernel, and the kernel context-switches back when the thread resumes.

## Debugging With the Kernel Model

When a model behaves strangely, classify the problem based on the LRM phases:

- **Evaluation issue**: wrong sensitivity, method ran too early, method initialized unexpectedly.
- **Update issue**: signal write not visible until update phase. Did you read immediately after writing in the same process?
- **Delta issue**: event notified in a later delta than expected, causing off-by-one delta delays.
- **Time issue**: timed wait or clock period mismatch.
- **Termination issue**: no timed events remain (simulation ends prematurely), or a process never yields (infinite loop blocking the evaluate phase).

## Under the Hood: The `sc_simcontext::crunch()` Loop
The SystemC scheduler operates inside `sc_simcontext::crunch()`. It is a strict state machine:
1. **Evaluate Phase**: The kernel pops processes from the `m_runnable` list and executes them. For an `SC_METHOD`, it calls the function. For an `SC_THREAD`, it performs a context switch via QuickThreads to resume the thread.
2. **Update Phase**: The kernel iterates over `m_update_list` (channels that had `request_update()` called) and invokes `update()`.
3. **Delta Notification Phase**: Events notified with zero delay (`SC_ZERO_TIME`) are processed. Sensitive processes are added to the `m_runnable` list. If the list is non-empty, the delta cycle repeats.
4. **Time Advance Phase**: If `m_runnable` is empty, the kernel advances simulation time (`m_curr_time`) to the timestamp of the next earliest event in `m_timed_events`, and triggers those events.

## IEEE 1666-2023 LRM: Formal Elaboration and Simulation Semantics

To truly master the SystemC simulation kernel, we must look at the formal rules established by the IEEE 1666-2023 Language Reference Manual (LRM). Chapter 4 of the LRM strictly defines Elaboration and Simulation Semantics. Let's map these formal rules to the practical behavior we observed above and dive into the specific clauses that govern the Accellera source code.

### Elaboration Phase (LRM Section 4.1)

Before simulation starts (via `sc_start`), the application undergoes **Elaboration**. Elaboration is the construction of the module hierarchy, instantiation of ports and channels, and the registration of processes. The LRM defines this phase in multiple distinct steps:

1. **Instantiation:** Modules, ports, primitives, and hierarchical channels are instantiated. The constructors of all `SC_MODULE` elements execute.
2. **Process Registration:** Calls to `SC_METHOD`, `SC_THREAD`, and `SC_CTHREAD` macros register the member functions with the simulation kernel (`sc_simcontext`). Static sensitivity is established via the `sensitive` stream.
3. **Port Binding:** Ports are bound to channels or other ports (LRM 4.1.3). The kernel checks port binding rules recursively until every port is bound to a channel or interface.
4. **End of Elaboration Callbacks:** The `end_of_elaboration()` callback is invoked on all instantiated modules and channels (LRM 4.1.4). This allows objects to execute initialization logic that depends on the fully constructed hierarchy but must run before simulation begins.

Any attempt to dynamically spawn processes (via `sc_spawn`) during elaboration is governed by strict rules, but generally, the hierarchy must be static by the time the kernel transitions to simulation.

### The Simulation Kernel Phases (LRM Section 4.2.1)

The LRM defines the simulation loop as a sequence of deterministic steps operating on non-deterministic process execution. The state machine defined in the standard maps directly to the Accellera SystemC reference implementation.

#### Initialization Phase (LRM 4.2.1.1)

When `sc_start()` is called, the kernel executes the **Initialization Phase** precisely once:
1. **Initialize Values:** The initial values of all primitive channels are evaluated.
2. **Start of Simulation Callbacks:** `start_of_simulation()` is invoked on all modules and channels.
3. **Process Execution:** Every registered method and thread process is made runnable (added to the initial evaluate queue) *unless* `dont_initialize()` was called on the process during elaboration.
4. **Update Phase Execution:** Any updates requested during initialization (e.g., initial signal writes) are executed.
5. **Delta Notifications:** Any zero-delay notifications are triggered.

#### Evaluate Phase (LRM 4.2.1.2)

The Evaluate Phase is where the "heavy lifting" happens. The kernel maintains a set of **runnable processes**.

*   **Rule of Non-determinism:** The LRM states that the order of execution of ready processes in the evaluate phase is undefined. The Accellera implementation often pops processes from a LIFO stack or a FIFO queue depending on the internal kernel version, but you **must never** rely on this order.
*   **Execution Semantics:**
    *   A runnable `SC_METHOD` executes from the beginning of its registered function until it returns.
    *   A runnable `SC_THREAD` (or `SC_CTHREAD`) resumes from its last suspension point (`wait()`) and executes until it suspends again or terminates.
*   **Immediate Notifications:** If a process executes `notify()` (with no time argument) on an `sc_event`, it triggers an **immediate notification** (LRM 4.2.1.2). Any process sensitive to this event is immediately added to the set of runnable processes *in the current evaluate phase*. This can potentially cause an infinite loop if processes immediately trigger each other without yielding.

#### Update Phase (LRM 4.2.1.3)

After the set of runnable processes becomes empty, the Evaluate phase terminates. The kernel transitions to the **Update Phase**.

*   **Primitive Channels:** During the evaluate phase, processes may have called `request_update()` on primitive channels. The kernel iterates through these channels exactly once and calls their virtual `update()` method.
*   **State Resolution:** For `sc_signal`, the `update()` method checks if the newly proposed value differs from the current value. If it does, the current value is overwritten, and a *value-changed event* is notified.

#### Delta Notification Phase (LRM 4.2.1.4)

After the Update Phase finishes, the kernel checks for delta notifications.

*   **Zero-delay Notifications:** If an event was notified using `notify(SC_ZERO_TIME)` or if a signal value change triggered an event, these are evaluated.
*   **Process Wakeup:** Processes sensitive to these events are marked as runnable.
*   **Delta Cycle Loop:** If the set of runnable processes is non-empty after evaluating delta notifications, the kernel increments the delta cycle count (`sc_delta_count()`) and immediately loops back to the Evaluate Phase. Time does **not** advance.

#### Time Advance Phase (LRM 4.2.1.5)

If the delta notification phase yields an empty set of runnable processes, the kernel checks for pending timed notifications.

*   **Finding the Next Event:** The kernel inspects the queue of timed events (e.g., `notify(10, SC_NS)` or `wait(10, SC_NS)`) and determines the earliest future timestamp.
*   **Advancing Time:** Simulation time (`sc_time_stamp()`) is advanced to this timestamp.
*   **Waking Timed Processes:** All events scheduled for this new time are triggered, moving sensitive processes back to the runnable set.
*   **Loop:** The kernel increments the delta cycle counter and returns to the Evaluate Phase.
*   **Termination:** If there are no pending timed events, or if the simulation time reaches the maximum time passed to `sc_start()`, simulation finishes. `end_of_simulation()` callbacks are invoked.

## Deep Dive: Accellera SystemC Source Implementation (`sysc/kernel`)

Understanding the LRM is crucial, but looking at how the Accellera source code implements these rules grounds our knowledge in reality. The simulation loop lives within the `sysc/kernel` directory.

### The Brain of the Simulator: `sc_simcontext`

The central class coordinating all simulation is `sc_simcontext` (found in `sysc/kernel/sc_simcontext.cpp`). When you call `sc_start()`, you are ultimately invoking `sc_get_curr_simcontext()->simulate()`, which delegates to the `crunch()` function.

Let's peek at the conceptual structure of the `crunch()` loop inside the Accellera implementation:

```cpp
// Conceptual representation of sc_simcontext::crunch() in sysc/kernel/sc_simcontext.cpp
inline void sc_simcontext::crunch( bool once ) {
    while ( true ) {
        // 1. EVALUATE PHASE
        while ( m_runnable->is_empty() == false ) {
            sc_process_b* next_process = m_runnable->pop_front();

            // Execute the process (calls run() on method or thread)
            m_current_writer = next_process;
            next_process->semantics();
            m_current_writer = 0;
        }

        // 2. UPDATE PHASE
        if ( !m_update_list->is_empty() ) {
            sc_update_mark_t* update_ptr = m_update_list;
            m_update_list = 0; // Clear for next cycle
            // Iterate and call update() on primitive channels
            while( update_ptr ) {
                update_ptr->update();
                update_ptr = update_ptr->next;
            }
        }

        // 3. DELTA NOTIFICATION PHASE
        if ( m_delta_events->is_empty() == false ) {
            // Process SC_ZERO_TIME notifications
            m_delta_events->process_events();
        }

        // 4. CHECK FOR NEXT CYCLE OR TIME ADVANCE
        if ( m_runnable->is_empty() ) {
            // No delta events fired, break loop to advance time
            break;
        }

        // Increment delta count before looping back to Evaluate
        m_delta_count++;
    }
}
```

### Process Management: `sc_process_b`, `sc_method_process`, `sc_thread_process`

The Accellera implementation models processes using a base class `sc_process_b` (defined in `sysc/kernel/sc_process.h`).
*   **`sc_method_process`**: This class represents an `SC_METHOD`. Its `semantics()` execution simply invokes the user's C++ function. Because it cannot yield, it requires no execution stack state management.
*   **`sc_thread_process`**: This class represents an `SC_THREAD`. Managing its execution requires an underlying coroutine library. Historically, Accellera SystemC uses QuickThreads (found in `sysc/qt`). When `sc_thread_process::semantics()` is called, the kernel initiates a thread context switch (saving the kernel's CPU registers and stack pointer, and restoring the thread's). When the thread calls `wait()`, it saves its state and context switches *back* to the kernel.

### The Update Mechanism: `sc_prim_channel`

How does the kernel know which signals to update? All primitive channels inherit from `sc_prim_channel` (`sysc/communication/sc_prim_channel.h`). When a process writes to an `sc_signal`, the signal's `write()` method calls `request_update()`.

The `request_update()` function adds the channel's pointer to the kernel's `m_update_list` queue. This list is drained and cleared at the start of the Update Phase, ensuring every channel is updated exactly once per delta cycle, no matter how many times it was written to during the Evaluate Phase.

### Event Notification: `sc_event`

The `sc_event` class (`sysc/kernel/sc_event.cpp`) is the backbone of synchronization.
When you call `e.notify()`, the implementation immediately iterates through the event's lists of statically and dynamically sensitive processes and pushes them onto the `m_runnable` queue.

When you call `e.notify(SC_ZERO_TIME)`, the event is pushed onto the kernel's `m_delta_events` list, deferring the wakeup until the Delta Notification Phase.

When you call `e.notify(10, SC_NS)`, the event is wrapped in a structure and inserted into a priority queue (`m_timed_events`) ordered by future execution time.

### Why this Architecture Matters for Modeling

Understanding this architecture is not just academic; it dictates how you write robust hardware models:

1. **Lock-Ups and Infinite Loops:** If you write an `SC_METHOD` that contains a `while(true)` loop without a `wait()` (which is illegal in methods anyway), you trap the simulation kernel inside the Evaluate Phase forever. Time will never advance.
2. **Immediate vs. Zero-Delay Notifications:** Using immediate `notify()` creates an immediate feedback loop. If Process A immediately notifies Event X, and Process B is sensitive to X, B runs in the *same* evaluate phase. If B then modifies state that triggers Process A, you have a zero-delay infinite loop that never even reaches the Update Phase. Always prefer delta notifications (`notify(SC_ZERO_TIME)`) or channel updates (`sc_signal`) for feedback loops to allow the kernel to breathe and resolve state.
3. **Wait State Overhead:** Because `SC_THREAD`s require memory allocations for thread stacks and context switching overhead (QuickThreads), excessive use of threads can slow down simulations. This is why high-performance TLM models predominantly use `b_transport` (which borrows the caller's thread) and `SC_METHOD`s for synchronization.

By understanding the IEEE 1666 rules and the Accellera implementation, you can debug lockups, race conditions, and performance bottlenecks by mentally tracing the `sc_simcontext::crunch()` loop.

## Source-reading checkpoint

Keep `tlm_core` nearby when kernel timing and TLM timing meet. A transport call is ordinary C++, but annotated delay eventually changes what the scheduler must do.

## Lesson 24: Source Deep Dive: Ports, Signals, and TLM Sockets

Canonical lesson: https://www.learn-systemc.com/tutorials/023-source-deep-dive-ports-signals-and-tlm-sockets

How the library turns interface binding, deferred updates, and transaction sockets into usable APIs.

## How to Read This Lesson

This is a source-reading lesson. We will use the Accellera implementation as a microscope, while keeping the LRM as the portability contract.

Three source areas explain most user-facing SystemC behavior: ports, signals, and TLM sockets. This section looks at the mechanics defined by the IEEE 1666 standard and how they map to C++.

## Standard and source context

## Ports Are Typed Access Points

At the API level, a port looks like a simple templated object, but at elaboration time, that port must bind to a channel implementing the specified interface. The LRM dictates strict port binding rules to ensure the topological integrity of the system before simulation begins.

## Exports Turn Hierarchy Inside Out

An export exposes an interface from a module boundary to its parent or peers, but the actual implementation of that interface is provided by a child module or channel instantiated within.

## Signals Are Deferred-Update Channels

To understand how `sc_signal` provides deferred updates in accordance with LRM semantics, we can build a complete, custom primitive channel that mimics its evaluate-update behavior.

```cpp
#include <systemc>

using namespace sc_core;

// 1. Define the Interface
template <typename T>
struct custom_signal_if : virtual public sc_interface {
  virtual const T& read() const = 0;
  virtual void write(const T&) = 0;
  virtual const sc_event& default_event() const = 0;
};

// 2. Implement the Primitive Channel
template <typename T>
class custom_signal : public sc_prim_channel, public custom_signal_if<T> {
private:
  T m_current_value;
  T m_new_value;
  sc_event m_value_changed;

public:
  explicit custom_signal(const char* name) : sc_prim_channel(name), m_current_value(T()), m_new_value(T()) {}

  const T& read() const override {
    return m_current_value;
  }

  void write(const T& val) override {
    if (val != m_new_value) {
      m_new_value = val;
      request_update(); // Register with the kernel for the update phase
    }
  }

  const sc_event& default_event() const override {
    return m_value_changed;
  }

protected:
  void update() override {
    // Called by the kernel during the update phase
    if (m_current_value != m_new_value) {
      m_current_value = m_new_value;
      m_value_changed.notify(SC_ZERO_TIME); // Delta notification
    }
  }
};

// 3. Test Module
SC_MODULE(TestModule) {
  sc_port< custom_signal_if<int> > port{"port"};

  SC_CTOR(TestModule) {
    SC_THREAD(run);
  }

  void run() {
    port->write(42);
    // Notice that read() still returns 0 here because update() hasn't happened yet!
    std::cout << "Immediate read: " << port->read() << "\n";
    wait(SC_ZERO_TIME);
    // After a delta cycle, the update phase has run
    std::cout << "Read after delta: " << port->read() << "\n";
  }
};

int sc_main(int, char*[]) {
  custom_signal<int> sig("sig");
  TestModule mod("mod");
  mod.port(sig);

  sc_start();
  return 0;
}
```

The real `sc_signal` handles writer policies, tracing, reset integration, and specialized logic types, but the core LRM mechanism relies directly on `sc_prim_channel`, `request_update()`, and the virtual `update()` callback.

## TLM Sockets Package Binding Patterns

TLM sockets are built to make transaction-level binding ergonomic. They inherit from both a port (to make outbound calls) and an export (to receive inbound calls).

Utility sockets such as `tlm_utils::simple_target_socket` let you register a member function as the transport callback. We can demonstrate this wrapping inside a complete compilable example.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>

using namespace sc_core;

SC_MODULE(Memory) {
  tlm_utils::simple_target_socket<Memory> socket{"socket"};

  SC_CTOR(Memory) {
    socket.register_b_transport(this, &Memory::b_transport);
  }

  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    trans.set_response_status(tlm::TLM_OK_RESPONSE);
    std::cout << "Target received TLM command at " << sc_time_stamp() << "\n";
  }
};

SC_MODULE(Cpu) {
  tlm_utils::simple_initiator_socket<Cpu> socket{"socket"};
  SC_CTOR(Cpu) { SC_THREAD(run); }
  void run() {
    tlm::tlm_generic_payload trans;
    sc_time delay = SC_ZERO_TIME;
    trans.set_command(tlm::TLM_WRITE_COMMAND);
    socket->b_transport(trans, delay);
  }
};

int sc_main(int, char*[]) {
  Cpu cpu("cpu");
  Memory mem("mem");
  cpu.socket.bind(mem.socket);
  sc_start();
  return 0;
}
```

That single `bind()` line hides a lot of structure. The socket has to expose the target interface, receive calls from an initiator, and dispatch them to your callback with the payload and delay, complying with the IEEE 1666-2023 standard for TLM-2.0 core interfaces.

## Under the Hood: Multi-Port Binding Policies
The `sc_port` class template takes three arguments: `sc_port<IF, N, POL>`.
- `IF`: The interface being bound.
- `N`: The maximum number of channels that can be bound to this port. Default is 1.
- `POL`: The binding policy.
In `sysc/communication/sc_port.h`, the policy `SC_ONE_OR_MORE_BOUND` (default) ensures that if the port is not bound by the end of elaboration, an exception is thrown. If you change it to `SC_ZERO_OR_MORE_BOUND`, the port can safely be left dangling. When `N > 1`, the port internally stores a `std::vector` of interface pointers, allowing you to index into them: `port[0]->read()`, `port[1]->read()`.

## IEEE 1666-2023 LRM: Interfaces, Ports, and Channels

The separation of computation and communication is central to SystemC. The LRM formalizes this via interfaces, ports, and channels.

### Interfaces (LRM Section 5.11)

An **interface** in SystemC is defined as an abstract C++ class derived from `sc_interface`. According to LRM Section 5.11.2, an interface class declares a set of pure virtual methods. It contains no state and no implementation.

In the Accellera implementation, `sc_interface` is defined in `sysc/communication/sc_interface.h`. Its only functional member is a virtual method `register_port()`. This method allows the interface (when implemented by a channel) to track which ports are connected to it, enabling static rule checking during elaboration.

### Channels (LRM Section 5.12)

A **channel** is a class that implements one or more interfaces. The LRM distinguishes between two types:
*   **Primitive Channels (LRM 5.12.2):** Inherit from `sc_prim_channel`. They do not have visible structure (no sub-modules or ports). They are permitted to use the `request_update()` and `update()` mechanism to interact directly with the scheduler's evaluate-update phases. `sc_signal`, `sc_mutex`, and `sc_fifo` are predefined primitive channels.
*   **Hierarchical Channels (LRM 5.12.3):** Inherit from `sc_module` (or `sc_channel`). They have structure (they can contain ports, sub-modules, and processes). They cannot directly use `request_update()`.

### Ports (LRM Section 5.13)

A **port** is the mechanism by which a module requires an interface. It is implemented via the `sc_port` template.
The LRM dictates strict **Binding Rules** (LRM 4.1.3):
1.  A port must be bound to a channel that implements the port's interface type, OR to another port of a parent module (hierarchical binding), OR to an export.
2.  Binding occurs during elaboration. Once simulation starts, port bindings cannot change.
3.  The port binding policy (`SC_ONE_OR_MORE_BOUND`, `SC_ZERO_OR_MORE_BOUND`, `SC_ALL_BOUND`) determines whether elaboration succeeds based on how many channels are bound to the port.

#### `sc_port` Internals (`sysc/communication/sc_port.cpp`)

When you call `port.bind(channel)` or use `port(channel)`, you are invoking `sc_port_b::bind()`. Internally, the Accellera kernel maintains a linked list of binding deferred actions. Actual resolution of bindings happens recursively just before the `end_of_elaboration` callback. The kernel traces the binding hierarchy: if Port A binds to Port B, and Port B binds to Channel C, the kernel ultimately resolves Port A directly to the interface pointer of Channel C.

When you invoke a method via a port (e.g., `port->read()`), the overloaded `operator->` simply returns the cached interface pointer to the bound channel. This makes port indirection extremely fast during simulationâ€”it's just a virtual function call.

### Exports (LRM Section 5.14)

An **export** (`sc_export`) allows a module to *provide* an interface to its parent or peers, forwarding calls to a channel instantiated *inside* the module.
Unlike ports, where the caller lives inside the module and the channel is outside, an export receives calls from outside and forwards them inside. This is essential for TLM targets.

The binding mechanism for exports (`sc_export::bind()`) resolves similarly to ports during elaboration. Ultimately, an external port bound to a module's export is resolved directly to the internal channel's interface pointer, eliminating any runtime overhead from crossing the module boundary.

### Signals and the `sc_signal` Family (LRM Section 6.4)

`sc_signal<T>` is the fundamental predefined primitive channel. It implements both `sc_signal_in_if<T>` and `sc_signal_inout_if<T>`.

#### Writer Policies (LRM 6.4.4)
The LRM defines `sc_writer_policy` to handle multiple processes writing to the same signal:
*   `SC_ONE_WRITER` (Default): Only one process is allowed to write to the signal during the entire simulation. The kernel enforces this by tracking the process ID of the first writer and throwing an exception if another process attempts to write.
*   `SC_MANY_WRITERS`: Multiple processes can write. However, if they write in the *same delta cycle*, the last write wins, which is often a race condition.
*   `SC_UNCHECKED_WRITERS`: The kernel performs no writer checking, optimizing performance but leaving you vulnerable to hard-to-debug race conditions.

#### Source Implementation of `sc_signal::write()`
In `sysc/communication/sc_signal.cpp`, the `write()` function looks conceptually like this:
```cpp
template <class T, sc_writer_policy POL>
inline void sc_signal<T, POL>::write( const T& value_ ) {
    // 1. Writer policy check (throws if violated)
    bool result = policy_type::check_write( this, sc_get_current_process_b() );

    // 2. Value comparison
    if ( !(m_new_val == value_) ) {
        m_new_val = value_;
        // 3. Request update from kernel
        this->request_update();
    }
}
```
And the corresponding `update()` called by the kernel:
```cpp
template <class T, sc_writer_policy POL>
inline void sc_signal<T, POL>::update() {
    // 1. Writer policy check (throws if violated)
    policy_type::update();

    if ( !(m_val == m_new_val) ) {
        m_val = m_new_val;
        // 2. Notify events
        m_value_changed_event.notify_delayed();
        m_default_event.notify_delayed();
    }
}
```

## TLM-2.0 Sockets (LRM Chapter 16)

The TLM-2.0 standard is built on top of the SystemC core interface/port/export mechanism.

### What is a Socket?

A TLM-2.0 socket is a structural object that groups together multiple interfaces required for transaction-level modeling. Specifically, an initiator socket acts as an `sc_port` for the forward path (`b_transport`, `nb_transport_fw`) and an `sc_export` for the backward path (`nb_transport_bw`). A target socket is the inverse.

#### Core Interfaces (LRM 11.1)
*   **`tlm_fw_transport_if`**: Implemented by targets. Contains `b_transport`, `nb_transport_fw`, `get_direct_mem_ptr`, and `transport_dbg`.
*   **`tlm_bw_transport_if`**: Implemented by initiators. Contains `nb_transport_bw` and `invalidate_direct_mem_ptr`.

### Socket Binding (LRM 16.1.4)

When you write `initiator_socket.bind(target_socket)`, the socket internally performs two SystemC core bindings:
1.  The initiator's internal `sc_port` is bound to the target's internal `sc_export` (Forward path).
2.  The target's internal `sc_port` is bound to the initiator's internal `sc_export` (Backward path).

### Convenience Sockets: `simple_target_socket`

The standard sockets (`tlm_initiator_socket` and `tlm_target_socket`) require the module to inherit from the transport interfaces and implement all virtual methods. This is verbose.

The `tlm_utils::simple_target_socket` (defined in `tlm_utils/simple_target_socket.h`) solves this. It inherits from `tlm_target_socket` but automatically implements the `tlm_fw_transport_if` itself. When the initiator calls `b_transport` on the socket's export, the socket's internal implementation forwards the call to a user-registered callback via a function pointer or `std::function`.

```cpp
// Conceptual view of simple_target_socket internal forwarding
void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) override {
    if (m_b_transport_functor) {
        // Call the user's registered function
        (*m_b_transport_functor)(trans, delay);
    } else {
        // Error: No callback registered
        SC_REPORT_ERROR("TLM-2", "b_transport not implemented");
    }
}
```

This brilliant use of C++ abstraction turns a strict, virtual-interface-heavy LRM standard into a simple callback registration API, hiding the complexity of export resolution and interface implementation from the user.

## Lesson 25: A Practical SystemC Source-Reading Workflow

Canonical lesson: https://www.learn-systemc.com/tutorials/024-a-practical-systemc-source-reading-workflow

How to move from public API behavior to the implementation without getting lost in templates.

## How to Read This Lesson

This is a source-reading lesson. We will use the Accellera implementation as a microscope, while keeping the LRM as the portability contract.

Reading SystemC source is not like reading a small application. It is a mature C++ library with compatibility concerns, macros, templates, platform code, and multiple abstraction layers. You need a workflow.

## Standard and source context

## Start From a Tiny Example

Pick one behavior:

```cpp
SC_METHOD(comb);
sensitive << a << b;
```

Ask one question: how does this method become runnable when `a` changes?

Now follow only the code needed for that question. Ignore unrelated template specializations until they become necessary.

## Trace Public API to Internal Object

For any feature, identify:

1. The public type or macro the user writes.
2. The object created by that API.
3. The simulation context it registers with.
4. The callback or virtual function invoked later.
5. The event or update that causes progress.

This turns source reading into a graph walk rather than a file-reading marathon.

## Use Search Terms That Match Concepts

Useful searches:

```bash
rg "request_update" src
rg "notify" src/sysc
rg "sc_simcontext" src
rg "SC_METHOD" src
rg "b_transport" src
```

When a term is too broad, search for a public method name plus a class name.

## Read Header First, Implementation Second

Headers show the API and object relationships. Implementation files show scheduling and policy.

For example, with signals:

1. Read the signal class declaration.
2. Find `write()`.
3. Find where it requests an update.
4. Find `update()`.
5. Find where value-change events are notified.

That sequence explains user-visible behavior.

## Keep a Source Notebook

For each source reading session, write down:

- public API
- internal type
- key member fields
- important callbacks
- scheduler interaction
- surprising edge cases

This site can grow those notes into new lessons. The best technical tutorials are not just explanations; they are maps of what the reader should inspect next.

## Exhaustive Deep Dive: Mapping the IEEE 1666 LRM to Accellera Source Architecture

Navigating the SystemC source effectively requires understanding how the **IEEE 1666-2023 LRM** specification maps to the directory structure and class hierarchy of the Accellera reference implementation. The standard dictates the *what* and *when*, while the reference implementation dictates the *how*.

### The LRM as the Ultimate Blueprint
The SystemC Standard is rigorously structured. When tracing a behavior, the first step is to locate the relevant LRM section:
- **Section 4 (Elaboration and Simulation Semantics):** Defines the phases of execution (`sc_start`, initialization, evaluate, update, delta cycles).
- **Section 5 (Processes):** Defines `SC_METHOD`, `SC_THREAD`, `SC_CTHREAD`, static/dynamic sensitivity, and the `wait()` semantics.
- **Section 6 (Core Language and Data Types):** Defines `sc_module`, ports, interfaces, and primitive channels (e.g., `sc_signal`, `sc_fifo`).
- **Section 10-16 (TLM-2.0):** Covers sockets, payloads, generic transport interfaces, and the Payload Event Queue (PEQ).

Whenever a behavior in the Accellera source seems convoluted, cross-referencing these chapters usually reveals that the complexity exists strictly to satisfy an edge-case compliance rule mandated by the LRM.

### The Accellera Source Directory Map
If you `cd` into the Accellera SystemC repository, the code is primarily divided into two main branches: `src/sysc` (Core SystemC) and `src/tlm_core` (Transaction Level Modeling).

**1. `src/sysc/kernel` (The Heart of the Simulator)**
This is where the magic happens. Key files include:
- `sc_simcontext.cpp`: The central engine. It manages the simulation time (`sc_time_stamp`), the list of all created objects (`m_object_manager`), and the event queues.
- `sc_runnable.cpp` / `sc_runnable.h`: Manages the lists of methods and threads that are ready to execute in the current delta cycle.
- `sc_module.cpp` / `sc_module_name.cpp`: Contains the logic for the structural hierarchy built during elaboration. The intricate `sc_module_name` class is responsible for pushing and popping module hierarchies onto the simulation context's internal stack so that nested modules get the correct hierarchical string names.
- `sc_event.cpp`: Implements the notification semantics (immediate, delta-delayed, timed).

**2. `src/sysc/communication` (Ports and Channels)**
This directory implements the core interfaces defined in LRM Section 6.
- `sc_signal.cpp` / `sc_clock.cpp`: Primitive channels implementing the evaluate-update paradigm.
- `sc_port.cpp` / `sc_export.cpp`: The structural binding mechanics.

**3. `src/sysc/qt` (QuickThreads)**
SystemC relies on user-space threading (coroutines) to implement `SC_THREAD`. The `qt` (QuickThreads) directory contains the assembly code (`md/` machine-dependent subdirectory) required to save and restore CPU registers, perform stack unwinding, and context-switch between threads in mere nanoseconds.

### Decompiling the Macros: `SC_MODULE` and `SC_CTOR`
A classic source-reading challenge is deciphering the omnipresent macros. According to `src/sysc/kernel/sc_module.h`:

```cpp
#define SC_MODULE(user_module_name) \
    struct user_module_name : ::sc_core::sc_module
```
This simply translates to inheriting from the core `sc_module` class. However, `SC_CTOR` is more complex:

```cpp
#define SC_CTOR(user_module_name) \
    typedef user_module_name SC_CURRENT_USER_MODULE; \
    user_module_name( ::sc_core::sc_module_name )
```
When you write `SC_CTOR(MyModule)`, it declares a constructor taking an `sc_module_name` object. The creation of `sc_module_name` triggers a constructor inside the Accellera kernel that registers the new module in the `sc_simcontext` hierarchy stack. This is why you cannot easily instantiate an `sc_module` without passing a string name; the kernel depends on the `sc_module_name` temporary object to hook into the elaboration tree.

### Tracing Process Registration (`SC_METHOD` / `SC_THREAD`)
When you declare an `SC_METHOD(func)`, what actually executes?
In `src/sysc/kernel/sc_process_macros.h`, `SC_METHOD` expands to an invocation of `sc_core::sc_module::declare_method_process()`.
This function takes:
1. The name of the function.
2. A macro-generated wrapper `sc_method_handle` (often using `SC_MAKE_FUNC_PTR`).
3. The host object (`this`).

The kernel then allocates an `sc_method_process` object (defined in `sc_method_process.h/cpp`), sets its state to UNINITIALIZED, and registers it with the `sc_simcontext`. During the initialization phase (LRM 4.2.1.1), the `sc_simcontext` iterates over all registered process objects and pushes them onto the runnable queue (unless `dont_initialize()` was called).

### Doxygen, ctags, and LSPs
Because SystemC relies heavily on macros and C++ templates, standard text searching (`grep` / `rg`) can sometimes fall short.
Professional SystemC architects heavily rely on:
- **Doxygen:** The Accellera repository includes a Doxygen configuration file. Generating the documentation yields a highly navigable HTML tree of the class hierarchies.
- **clangd / LSP:** Modern Language Servers can expand the macros in real-time. If you encounter an opaque type like `sc_event_or_list`, using `Go to Definition` via an LSP will immediately drop you into `sysc/kernel/sc_event.h`, showing exactly how dynamic event sensitivity is implemented using linked lists.

By mapping the rigid rules of the IEEE 1666 LRM to the highly optimized C++ structures of the Accellera source code, the seemingly opaque behavior of SystemC transforms into a readable, deterministic C++ architecture.

## Lesson 26: Source Deep Dive: SC_METHOD, SC_THREAD, and wait()

Canonical lesson: https://www.learn-systemc.com/tutorials/025-source-deep-dive-sc-method-sc-thread-and-wait

How process registration, static sensitivity, runnable queues, and wait-based suspension work conceptually.

## How to Read This Lesson

This is a source-reading lesson. We will use the Accellera implementation as a microscope, while keeping the LRM as the portability contract.

`SC_METHOD` and `SC_THREAD` look like simple macros, but they create very different process objects inside the kernel.

The implementation work behind those lines is the heart of SystemC execution according to the LRM.

## Standard and source context

## What Registration Must Capture

When a process is registered, the kernel needs the owning module, the member function pointer, the process kind, sensitivity, and execution state.

To prove how the kernel manages these different process types and runnable queues, here is a complete compilable example demonstrating static sensitivity and wait dynamics.

```cpp
#include <systemc>

using namespace sc_core;

SC_MODULE(ProcessInternalsDemo) {
  sc_event trigger_event;

  SC_CTOR(ProcessInternalsDemo) {
    // 1. SC_METHOD: A non-resumable callback
    SC_METHOD(method_process);
    sensitive << trigger_event;
    dont_initialize();

    // 2. SC_THREAD: A resumable coroutine with execution state
    SC_THREAD(thread_process);
  }

  // Conceptually similar to:
  // void run_once() { (owner->*callback)(); }
  void method_process() {
    std::cout << "[METHOD] Executing at " << sc_time_stamp() << "\n";
    // wait(); // ILLEGAL: Methods cannot suspend!
  }

  // Conceptually similar to a fiber context switch
  void thread_process() {
    std::cout << "[THREAD] Started at " << sc_time_stamp() << "\n";

    // Transfers control back to scheduler, removing process from runnable queue
    wait(10, SC_NS);

    std::cout << "[THREAD] Resumed at " << sc_time_stamp() << ". Firing event.\n";
    trigger_event.notify(SC_ZERO_TIME);
  }
};

int sc_main(int argc, char* argv[]) {
  ProcessInternalsDemo demo("demo");
  sc_start(50, SC_NS);
  return 0;
}
```

## SC_METHOD vs SC_THREAD Internals

An `SC_METHOD` process runs to completion. It cannot call `wait()`, because the kernel does not preserve a suspended stack for it. It is merely a C++ callback function invoked by the kernel.

An `SC_THREAD` process can suspend. The kernel must resume this process after the wait condition is satisfied. That requires process state beyond a simple callback. Depending on the platform, the implementation uses coroutine or fiber-like mechanisms.

## wait() Changes Process State

`wait()` does not sleep the host OS thread. It tells the SystemC scheduler: this process is no longer runnable until a condition is met.

Each call to `wait()` installs a wait condition, removes the process from the runnable set, and transfers control back to the scheduler.

## Runnable Queues

At simulation time, the scheduler keeps track of runnable processes. A simplified view:

1. Pop process from runnable queue.
2. Transfer control to the process (`run()` or context switch).
3. Process returns or yields (`wait()`).
4. Once runnable queue is empty, trigger the update phase.

When you know the internal shape, the rules stop feeling arbitrary. SystemC lets you write ordinary-looking C++, but the kernel is building a small event-driven operating environment around your objects.

## Exhaustive Deep Dive: IEEE 1666-2023 LRM and Accellera Process Architecture

To truly understand how SystemC executes concurrently, we must look at **IEEE 1666-2023 LRM Section 5 (Processes)** and how the Accellera kernel implements these specifications in `src/sysc/kernel`.

### LRM Section 5: Process Semantics
The LRM defines two primary simulation processes: **Methods** and **Threads** (and the deprecated `SC_CTHREAD`).
- **LRM Clause 5.2.2 (Method processes):** Executes from start to finish without blocking. It returns control to the simulator.
- **LRM Clause 5.2.3 (Thread processes):** May call `wait()` to suspend execution. Its local variables and execution state are preserved across suspensions.

The LRM specifies that the simulator determines which processes to execute during the **Evaluation Phase (LRM 4.2.1.2)** based on the set of runnable processes.

### Inside `sysc/kernel`: `sc_process_b` Base Class
Both `SC_METHOD` and `SC_THREAD` expand to macros that dynamically allocate process objects. In the Accellera source (`src/sysc/kernel/sc_process.h`), all process types inherit from `sc_process_b`.

```cpp
class sc_process_b {
    // ...
    sc_process_state m_state;     // e.g., RUNNABLE, SLEEPING, TERMINATED
    sc_event_list*   m_event_list; // What this process is dynamically waiting on
    sc_process_b*    m_runnable_nxt; // Intrusive linked list for the scheduler
    // ...
};
```
When an event occurs (e.g., `trigger_event.notify()`), the kernel checks its list of sensitive processes. If a process matches, the kernel pushes it onto the `sc_runnable` list (defined in `sc_runnable.h`). The scheduler's evaluation loop just pops items off this intrusive linked list and executes them.

### `sc_method_process` vs `sc_thread_process`
In `src/sysc/kernel/sc_method_process.h`, the implementation is surprisingly simple. When a method is popped from the runnable queue, the kernel essentially calls a C++ member function pointer:
```cpp
inline void sc_method_process::execute() {
    m_semantics_method->invoke( m_semantics_host );
}
```
Because it is a standard C++ function call, the C++ call stack is used. If you try to call `wait()` inside a method, the kernel's `wait` implementation checks `sc_get_current_process_b()->process_kind()` and throws an `SC_ID_WAIT_NOT_ALLOWED_` exception because there is no mechanism to save the stack.

In `src/sysc/kernel/sc_thread_process.h`, the implementation is vastly different. A thread requires a **coroutine**.

### QuickThreads and User-Space Context Switching
SystemC cannot map `SC_THREAD` to `std::thread` or POSIX `pthread` because OS-level context switching is non-deterministic and takes thousands of cycles per switch. Hardware models may have millions of threads context-switching every simulated nanosecond.

Instead, the Accellera kernel implements **QuickThreads** (located in `src/sysc/qt/`). QuickThreads is a library of bare-metal assembly code (`qt/md/`) that implements cooperative user-space threads (fibers/coroutines).

When you call `wait(10, SC_NS)` in an `SC_THREAD`:
1. The kernel adds the thread to a time-based queue (`sc_simcontext::m_time_events`).
2. The thread invokes `sc_cor_qt::yield()`.
3. The underlying QuickThreads assembly code executes. For x86_64 (`src/sysc/qt/md/i386.s` or similar), it pushes the instruction pointer (`RIP`), base pointer (`RBP`), and all callee-saved registers onto the *thread's own allocated stack memory*.
4. It then loads the `sc_simcontext` scheduler's saved stack pointer into the CPU's stack pointer register (`RSP`).
5. Execution jumps back to the scheduler loop.

When the 10 ns elapse, the scheduler pops the thread, does the reverse context switch (swapping `RSP` back to the thread's stack), and pops the CPU registers. The C++ code resumes cleanly exactly where `wait()` was called.

### Dynamic Sensitivity and the `sc_event_or_list`
When you write `wait(e1 | e2)`, you create dynamic sensitivity (LRM 5.2.14).
In the source, `operator|` on `sc_event` objects returns an `sc_event_or_list`. The kernel stores this list inside `m_event_list` of the `sc_thread_process`. When *either* event fires, the kernel checks this list, marks the thread as runnable, and clears the list. This is why you must re-issue `wait(e1 | e2)` in a `while` loop if you want to wait again.

Understanding this architecture is crucial: `SC_METHOD` is a fast, C++ callback. `SC_THREAD` is a cooperative assembly-level coroutine with an isolated stack. Choose wisely based on the simulation performance required.

## Lesson 27: Source Deep Dive: Port Binding and Interface Dispatch

Canonical lesson: https://www.learn-systemc.com/tutorials/026-source-deep-dive-port-binding-and-interface-dispat

How sc_port, sc_export, sc_interface, and channels connect modules during elaboration.

## How to Read This Lesson

This is a source-reading lesson. We will use the Accellera implementation as a microscope, while keeping the LRM as the portability contract.

Port binding is where SystemC turns a C++ object hierarchy into a connected model graph. The implementation must answer what interface is required, what object provides it, and if the binding is complete.

## Standard and source context

## Interfaces, Ports, and Channels

An interface derives from `sc_interface`. A port requires an interface. A channel implements the interface. Binding must happen before simulation time begins, allowing the kernel to validate the structural integrity.

Here is a complete, compilable example illustrating how an `sc_port` resolves a C++ virtual function call to an `sc_interface` implemented by an `sc_export` mapping to a channel.

```cpp
#include <systemc>
#include <iostream>

using namespace sc_core;

// 1. The Interface Contract
struct BusIf : public sc_interface {
  virtual uint32_t read(uint64_t address) = 0;
  virtual void write(uint64_t address, uint32_t data) = 0;
};

// 2. The Channel Implementation
class MemoryChannel : public sc_module, public BusIf {
public:
  SC_CTOR(MemoryChannel) {}

  uint32_t read(uint64_t address) override {
    std::cout << "Memory handling READ at " << std::hex << address << "\n";
    return 0xCAFE;
  }
  void write(uint64_t address, uint32_t data) override {
    std::cout << "Memory handling WRITE at " << std::hex << address << " data: " << data << "\n";
  }
};

// 3. The Subsystem Exposing the Channel via sc_export
SC_MODULE(MemorySubsystem) {
  sc_export<BusIf> target{"target"};
  MemoryChannel memory{"memory"};

  SC_CTOR(MemorySubsystem) {
    // Export binds to the internal channel implementation
    target.bind(memory);
  }
};

// 4. The Initiator Using the Channel via sc_port
SC_MODULE(CpuModel) {
  sc_port<BusIf> bus{"bus"};

  SC_CTOR(CpuModel) { SC_THREAD(run); }

  void run() {
    // The operator-> provides access to the bound interface
    // The call becomes a virtual function call on the channel implementation
    bus->write(0x1000, 0x1234);
    uint32_t val = bus->read(0x1000);
  }
};

int sc_main(int argc, char* argv[]) {
  CpuModel cpu("cpu");
  MemorySubsystem subsystem("subsystem");

  // Elaboration phase: Structural binding
  cpu.bus.bind(subsystem.target);

  sc_start(); // Simulation phase
  return 0;
}
```

## Binding During Elaboration

Binding happens before simulation. That matters because the kernel can validate topology before any process runs. At the end of elaboration, the kernel checks that required ports are bound and that binding policies are satisfied.

## Calling Through a Port

Once bound, the module can call through the port using `operator->`. The useful abstraction: structural binding is checked during elaboration, while actual behavior is ordinary C++ interface dispatch (virtual function calls) during simulation.

## How This Relates to TLM Sockets

TLM sockets are higher-level wrappers around the exact same ideas. An initiator socket contains a port to a forward transport interface. A target socket exposes an export for that interface and dispatches calls to registered callbacks. The magic is packaging, not a different universe.

## Lesson 28: Source Deep Dive: sc_signal Update Internals

Canonical lesson: https://www.learn-systemc.com/tutorials/027-source-deep-dive-sc-signal-update-internals

How writes, current values, pending values, update requests, and value-change events work.

## How to Read This Lesson

This is a source-reading lesson. We will use the Accellera implementation as a microscope, while keeping the LRM as the portability contract.

`sc_signal<T>` is one of the best source-reading targets because it demonstrates the SystemC kernel contract for primitive channels, specifically LRM Section 6.15.

The user simply writes `sig.write(next_value);`, but hardware-like behavior requires more than assigning a C++ variable.

## Standard and source context

## The Two-Value Model and Primitive Channels

A signal has a current value and a pending value. To understand exactly how the SystemC simulation context manages this, we can write a complete, compilable primitive channel that implements the LRM's `request_update()` and `update()` semantics without hiding behind the built-in `sc_signal`.

```cpp
#include <systemc>

using namespace sc_core;

// 1. The Interface
template <typename T>
struct custom_signal_if : virtual public sc_interface {
  virtual const T& read() const = 0;
  virtual void write(const T&) = 0;
  virtual const sc_event& default_event() const = 0;
};

// 2. The Primitive Channel mimicking sc_signal
template <typename T>
class SignalInternalsDemo : public sc_prim_channel, public custom_signal_if<T> {
private:
  T m_current_value;
  T m_new_value;
  sc_event m_value_changed;

public:
  explicit SignalInternalsDemo(const char* name)
    : sc_prim_channel(name), m_current_value(T()), m_new_value(T()) {}

  const T& read() const override {
    return m_current_value;
  }

  void write(const T& value) override {
    // Check if the value is actually changing
    if (value != m_new_value) {
      m_new_value = value;
      // This tells the simcontext to push this channel into the update queue
      request_update();
    }
  }

  const sc_event& default_event() const override {
    return m_value_changed;
  }

protected:
  // 3. The Update Callback (Called by the Kernel)
  void update() override {
    if (m_current_value != m_new_value) {
      m_current_value = m_new_value;
      // Notify sensitive processes in the next delta cycle
      m_value_changed.notify(SC_ZERO_TIME);
    }
  }
};

// 4. Test Module
SC_MODULE(SignalTest) {
  sc_port<custom_signal_if<int>> port{"port"};

  SC_CTOR(SignalTest) {
    SC_THREAD(writer_thread);
    SC_METHOD(reader_method);
    sensitive << port; // Binds to default_event()
    dont_initialize();
  }

  void writer_thread() {
    std::cout << "[Time: " << sc_time_stamp() << "] Writing 42\n";
    port->write(42);

    // Read immediately? It will be the OLD value.
    std::cout << "[Time: " << sc_time_stamp() << "] Immediate read: " << port->read() << "\n";

    wait(SC_ZERO_TIME); // Advance to next delta

    // Read after update phase
    std::cout << "[Time: " << sc_time_stamp() << "] Read after delta: " << port->read() << "\n";
  }

  void reader_method() {
    std::cout << "[Time: " << sc_time_stamp() << "] Reader method triggered. New value: " << port->read() << "\n";
  }
};

int sc_main(int argc, char* argv[]) {
  SignalInternalsDemo<int> sig("sig");
  SignalTest test("test");
  test.port(sig);

  sc_start(10, SC_NS);
  return 0;
}
```

The real implementation handles more details (writer policies, tracing, reset integration), but the visible behavior comes exactly from this sequence:
1. process writes signal
2. signal requests update
3. scheduler finishes evaluate phase
4. kernel calls `update()` on the channel
5. signal commits pending value and notifies value-change event
6. sensitive processes become runnable in a later delta

## Writer Checks and Resolved Signals

Signals enforce writer policies (LRM Section 6.15.3). If two processes drive one signal unexpectedly, the implementation reports an error. This is not just politeness; multiple writers can make a hardware model ambiguous.

Resolved signal types (`sc_resolved`, `sc_rv`) exist for multi-driver logic. They apply resolution rules to determine the final value. Use resolved signals for real multi-driver semantics (tri-state buses), not to silence accidental multiple-driver bugs.

## Debugging Signal Internals

If a signal value appears late:
- the writer may have only set the pending value
- the update phase may not have run yet
- the reader may be sensitive to the value-change event in the next delta

Once you understand the pending/current split and the evaluate-update cycle, most signal timing questions become explainable.

## Exhaustive Deep Dive: IEEE 1666-2023 LRM and Accellera Update Phase Source

The two-value evaluate/update paradigm of primitive channels is the cornerstone of modeling concurrent hardware in sequential C++. It is governed strictly by **IEEE 1666-2023 LRM Section 4.2 (Simulation semantics)** and **Section 6.15 (sc_signal)**.

### LRM Section 4.2.1: The Evaluate and Update Phases
According to the LRM, a single delta cycle has two distinct computational steps:
1. **Evaluation Phase:** The kernel executes all runnable processes (`SC_METHOD`, `SC_THREAD`). If a process calls `sig.write(val)`, the signal does *not* immediately change. It instead schedules itself for an update.
2. **Update Phase:** Once the runnable queue is empty, the scheduler pauses process execution. It then iterates through all primitive channels that requested an update and calls their `update()` virtual method.

This guarantees determinism. No matter the order in which two processes execute during the evaluate phase, they will both read the same "old" signal value.

### Source Code: `sc_prim_channel` and `request_update()`
If you trace the Accellera source code into `src/sysc/communication/sc_signal.h`, you'll see that `sc_signal<T>` inherits from `sc_signal_t<T, POL>`, which ultimately derives from `sc_prim_channel`.

When you call `write()`, it looks like this:
```cpp
template<class T, sc_writer_policy POL>
inline void sc_signal_t<T, POL>::write(const T& value) {
    // 1. Writer policy checks
    if (!m_writer_p->check_write(sc_get_current_process_b(), ...)) return;

    // 2. Check if pending value changed
    m_new_val = value;
    if (!(m_new_val == m_cur_val)) {
        // 3. Ask the kernel to call our update() later
        this->request_update();
    }
}
```

What exactly does `request_update()` do?
In `src/sysc/kernel/sc_prim_channel.cpp`, the channel accesses the central simulation context (`sc_simcontext`) and appends itself to an array:
```cpp
void sc_prim_channel::request_update() {
    sc_simcontext* simc = simcontext();
    simc->get_update_list()->push_back(this);
}
```

### The Scheduler Loop: `sc_simcontext::crunch()`
To see the update phase in action, you must read the core scheduler loop inside `src/sysc/kernel/sc_simcontext.cpp`. The method `crunch()` (which runs delta cycles) contains a loop roughly structured like this:

```cpp
while (true) {
    // --- EVALUATE PHASE ---
    while (!m_runnable->is_empty()) {
        sc_process_b* p = m_runnable->pop();
        p->execute(); // Runs SC_METHODs or resumes SC_THREADs
    }

    // --- UPDATE PHASE ---
    sc_prim_channel_registry* reg = m_prim_channel_registry;
    if (reg->pending_updates()) {
        reg->perform_update(); // Calls update() on all queued channels
    }

    // --- DELTA NOTIFICATION PHASE ---
    // Move zero-time notifications to the runnable queue for the NEXT delta
    // If nothing new is runnable, advance time!
}
```

When `perform_update()` runs, it calls `update()` on your `sc_signal`. The signal copies `m_new_val` into `m_cur_val` and calls `m_value_changed_event.notify(SC_ZERO_TIME)`. This event notification pushes sensitive processes onto the runnable queue, triggering another delta cycle!

### Thread Safety and `async_request_update()`
A critical limitation of the `sc_simcontext::crunch()` loop is that it is purely single-threaded.

**LRM Clause 4.2.1.8** introduced `async_request_update()` specifically to address multi-threading. If an external OS thread (e.g., a POSIX thread reading a network socket) tries to call `request_update()`, it will cause a race condition on the `get_update_list()->push_back(this)` logic inside the Accellera kernel, corrupting the simulation state.

To safely inject values from external threads, you must use `sc_prim_channel::async_request_update()`.
In the source code, this function locks a global mutex (`sc_host_mutex`), adds the channel to a specialized asynchronous update queue, and interrupts the SystemC scheduler using a thread-safe signaling mechanism (often a pipe or event). The scheduler then safely merges the async updates during its standard update phase.

Understanding the deep split between `m_cur_val` (the present), `m_new_val` (the future), and the rigidly scheduled `request_update()` queue is the key to mastering RTL-level modeling in SystemC.

## Lesson 29: Source Deep Dive: TLM Socket Internals

Canonical lesson: https://www.learn-systemc.com/tutorials/028-source-deep-dive-tlm-socket-internals

How simple initiator and target sockets wrap interfaces, callbacks, binding, and transport dispatch.

## How to Read This Lesson

This is a source-reading lesson. We will use the Accellera implementation as a microscope, while keeping the LRM as the portability contract.

TLM sockets are convenient because they hide a lot of interface plumbing. To understand them, we must peel back the convenience layer. According to the IEEE 1666-2023 LRM Section 12, a socket is structurally composed of an `sc_port` and an `sc_export` bound to forward and backward transport interfaces.

## Standard and source context

## The Interfaces Underneath

TLM defines core transport interfaces like `tlm_fw_transport_if` and `tlm_bw_transport_if`. A utility target socket (`simple_target_socket`) implements the forward interface internally and dispatches calls to your registered member function.

To see exactly what the socket does under the hood, here is a complete, fully compilable example where the target manually implements the raw `tlm_fw_transport_if` and uses standard `sc_export` to receive transactions, completely bypassing `simple_target_socket` for the target side!

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>

using namespace sc_core;

// 1. The Raw Core Interface Target (What simple_target_socket hides)
class RawMemoryTarget : public sc_module, public tlm::tlm_fw_transport_if<> {
public:
  // Expose the interface outward
  sc_export<tlm::tlm_fw_transport_if<>> target_export{"target_export"};

  SC_CTOR(RawMemoryTarget) {
    // Bind the export to 'this' module, which implements the interface
    target_export.bind(*this);
  }

  // --- Implement tlm_fw_transport_if ---

  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) override {
    std::cout << "[RawTarget] b_transport called with address 0x"
              << std::hex << trans.get_address() << "\n";
    trans.set_response_status(tlm::TLM_OK_RESPONSE);
  }

  tlm::tlm_sync_enum nb_transport_fw(tlm::tlm_generic_payload& trans,
                                     tlm::tlm_phase& phase, sc_time& delay) override {
    return tlm::TLM_COMPLETED;
  }

  bool get_direct_mem_ptr(tlm::tlm_generic_payload& trans,
                          tlm::tlm_dmi& dmi_data) override {
    return false;
  }

  unsigned int transport_dbg(tlm::tlm_generic_payload& trans) override {
    return 0;
  }
};

// 2. Standard Utility Initiator
SC_MODULE(CpuInitiator) {
  tlm_utils::simple_initiator_socket<CpuInitiator> socket{"socket"};

  SC_CTOR(CpuInitiator) { SC_THREAD(run); }

  void run() {
    tlm::tlm_generic_payload trans;
    sc_time delay = SC_ZERO_TIME;

    trans.set_command(tlm::TLM_WRITE_COMMAND);
    trans.set_address(0x1000);

    // operator-> on the socket accesses the bound tlm_fw_transport_if!
    socket->b_transport(trans, delay);
  }
};

int sc_main(int argc, char* argv[]) {
  CpuInitiator cpu("cpu");
  RawMemoryTarget mem("mem");

  // The initiator socket contains an sc_port which binds to the sc_export
  cpu.socket.bind(mem.target_export);

  sc_start();
  return 0;
}
```

When you use `target.register_b_transport(...)`, the utility socket creates a small internal channel object exactly like the `RawMemoryTarget` above, storing your object pointer and member function pointer, and dispatches the raw `b_transport` interface call into your callback.

## Initiator Socket

The initiator socket acts like a typed access point to the target's transport interface. When you call `socket->b_transport(...)`, that operator call reaches the bound target interface. The initiator does not need to know whether the target is a raw module implementing the interface or a utility socket doing callback dispatch.

## Generic Payload Lifetime

Sockets dispatch payload references. They do not magically copy all transaction data. That means payload lifetime and extension ownership matter.

For blocking transport, a stack payload is fine (as shown in the example). For non-blocking transport with deferred completion, the transaction may outlive the call. Then you need a disciplined ownership strategy, often with a memory manager.

## Why Socket Internals Matter

When a TLM model fails, the bug is often one of these:
- socket not bound
- callback not registered
- payload reused too early
- response status not set
- delay not updated consistently

Understanding sockets as standard `sc_port` binding plus callback dispatch makes those bugs much easier to diagnose.

## Lesson 30: Virtual Platform Patterns

Canonical lesson: https://www.learn-systemc.com/tutorials/029-virtual-platform-patterns

How to combine CPUs, memories, buses, peripherals, interrupts, and firmware-visible behavior.

## How to Read This Lesson

Treat this as engineering practice, not trivia. The patterns here are the ones that keep large models understandable after the original author has moved on.

A virtual platform is a SystemC model of a system that software can run against. It usually trades pin-level detail for speed and architectural usefulness.

## Standard and source context

Practice lessons should still cite their roots. Use `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for behavior, `Accellera SystemC GitHub repository/systemc` for the reference kernel, and `Accellera SystemC GitHub repository/systemc-common-practices` for reusable modeling patterns. The goal is to turn standard rules into habits that survive real project scale.

## Industry Standard Architectures

When building Virtual Platforms in SystemC, avoid proprietary or vendor-specific bus models unless strictly necessary. Instead, strictly model the architecture after the official Accellera TLM-2.0 loosely-timed (LT) and approximately-timed (AT) open-source examples, or the industry-standard Doulos Simple Bus.

Most standards-compliant platforms contain:
- CPU or instruction-set simulator wrapper (Initiator)
- memory map and address decoder (Interconnect/Bus)
- RAM and ROM models (Targets)
- timers, interrupt controllers, UARTs, DMA (Targets & Initiators)
- debug and tracing utilities

## Address Decoding & The Simple Bus

An interconnect routes transactions by address. To demonstrate this pattern, here is a complete, compilable, and standards-compliant TLM-2.0 Simple Router model. It uses the `simple_target_socket` to receive transactions from an initiator (e.g., a CPU) and forwards them to the correct target (e.g., Memory or UART) via an array of `simple_initiator_socket`s.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <vector>

using namespace sc_core;

// 1. The Targets
SC_MODULE(MemoryBlock) {
  tlm_utils::simple_target_socket<MemoryBlock> socket{"socket"};
  SC_CTOR(MemoryBlock) { socket.register_b_transport(this, &MemoryBlock::b_transport); }
  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    delay += sc_time(10, SC_NS);
    trans.set_response_status(tlm::TLM_OK_RESPONSE);
  }
};

SC_MODULE(UartBlock) {
  tlm_utils::simple_target_socket<UartBlock> socket{"socket"};
  SC_CTOR(UartBlock) { socket.register_b_transport(this, &UartBlock::b_transport); }
  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    delay += sc_time(50, SC_NS);
    trans.set_response_status(tlm::TLM_OK_RESPONSE);
    if (trans.get_command() == tlm::TLM_WRITE_COMMAND) {
      std::cout << "[UART] Char: " << (char)*(trans.get_data_ptr()) << "\n";
    }
  }
};

// 2. The Simple Bus / Interconnect
SC_MODULE(SimpleBus) {
  tlm_utils::simple_target_socket<SimpleBus> target_socket{"target_socket"};
  tlm_utils::simple_initiator_socket<SimpleBus> init_socket_mem{"init_socket_mem"};
  tlm_utils::simple_initiator_socket<SimpleBus> init_socket_uart{"init_socket_uart"};

  SC_CTOR(SimpleBus) {
    target_socket.register_b_transport(this, &SimpleBus::b_transport);
  }

  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    uint64_t addr = trans.get_address();

    // Address Map:
    // 0x0000 - 0x0FFF : Memory
    // 0x1000 - 0x1FFF : UART
    if (addr < 0x1000) {
      init_socket_mem->b_transport(trans, delay);
    } else if (addr < 0x2000) {
      // Localize address for target
      trans.set_address(addr - 0x1000);
      init_socket_uart->b_transport(trans, delay);
      // Restore address
      trans.set_address(addr);
    } else {
      trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
    }
  }
};

// 3. The CPU (Initiator)
SC_MODULE(CpuModel) {
  tlm_utils::simple_initiator_socket<CpuModel> socket{"socket"};
  SC_CTOR(CpuModel) { SC_THREAD(run); }
  void run() {
    tlm::tlm_generic_payload trans;
    sc_time delay = SC_ZERO_TIME;
    char data = 'A';

    // Write to UART
    trans.set_command(tlm::TLM_WRITE_COMMAND);
    trans.set_address(0x1000);
    trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
    trans.set_data_length(1);

    socket->b_transport(trans, delay);
    wait(delay); // Synchronize with simulation time
  }
};

int sc_main(int argc, char* argv[]) {
  CpuModel cpu("cpu");
  SimpleBus bus("bus");
  MemoryBlock mem("mem");
  UartBlock uart("uart");

  // Binding
  cpu.socket.bind(bus.target_socket);
  bus.init_socket_mem.bind(mem.socket);
  bus.init_socket_uart.bind(uart.socket);

  sc_start();
  return 0;
}
```

The bus checks the address, forwards the payload, and preserves timing annotations. Modifying the generic payload address is allowed, but you must restore it before the transaction returns to the caller.

## Timing Strategy

Choose timing deliberately:
- **Untimed**: fastest, good for pure software enablement.
- **Loosely timed (LT)**: good for early performance and firmware work, relies on `b_transport` and quantum keeping (temporal decoupling).
- **Approximately timed (AT)**: useful when ordering and protocol phases (e.g. `BEGIN_REQ`, `END_REQ`) matter.
- **Cycle-accurate**: slower, needed for detailed microarchitecture questions.

## Interrupts and Register Models

Interrupts can be signals, events, TLM messages, or register-visible state depending on the platform style. Firmware should observe a coherent interrupt controller behavior: pending bits, enables, priorities, acknowledge paths, and side effects.

Register behavior is where many virtual platforms become valuable. Model reset values, read-only bits, write-one-to-clear bits, reserved fields, side effects, and timing when needed.

## Under the Hood: TLM-2.0 Interfaces (`tlm_fw_transport_if`)
Virtual Platforms rely entirely on the TLM-2.0 transport interfaces. In `src/tlm_core/tlm_2/tlm_interfaces/tlm_fw_bw_ifs.h`, you'll find `tlm_fw_transport_if`.
This interface enforces the implementation of `b_transport`, `nb_transport_fw`, `get_direct_mem_ptr`, and `transport_dbg`.
By making these pure virtual, the Accellera TLM kernel enforces strict decoupling. An initiator socket holds a pointer to this interface, and the target socket binds its internal implementation to it. Thus, the virtual platform initiator is executing the target's C++ function via a vtable lookup, resulting in extreme execution speed (often millions of transactions per second) without relying on heavy SystemC kernel context switches.

## IEEE 1666-2023 LRM: TLM-2.0 Modeling and Architectures

When constructing a Virtual Platform (VP) in SystemC, the IEEE 1666-2023 standard provides a dedicated set of rules, interfaces, and patterns in Chapter 10 through 16 to ensure high interoperability, speed, and accuracy. The structure we created in the Simple Bus example directly aligns with these rules.

### Loosely-Timed (LT) vs Approximately-Timed (AT) (LRM 10.3)

The standard defines two primary modeling styles for virtual platforms based on the accuracy required:

#### Loosely-Timed (LT)
*   **Goal:** Maximum simulation speed for software execution.
*   **Mechanism:** Uses the blocking transport interface (`b_transport`). A single function call carries the transaction from initiator to target and back.
*   **Timing:** Uses **Temporal Decoupling** (LRM 10.3.1). Processes run ahead of simulation time and synchronize using a quantum. The target accumulates delay (e.g., `delay += sc_time(10, SC_NS)`) without suspending (`wait()`).
*   **Use Case:** Executing operating systems, firmware, and drivers where the exact bus cycle latency is less important than the architectural logic.

#### Approximately-Timed (AT)
*   **Goal:** Accurate architectural exploration and performance modeling.
*   **Mechanism:** Uses the non-blocking transport interface (`nb_transport_fw` and `nb_transport_bw`). A single transaction is broken into multiple phases (e.g., Request, Response) which are passed back and forth.
*   **Timing:** Targets and initiators use `wait()` or timed notifications to represent cycle delays. Components contend for shared resources, and arbitration rules are modeled.
*   **Phases (LRM 14.1.2):** The base protocol defines `BEGIN_REQ`, `END_REQ`, `BEGIN_RESP`, and `END_RESP`.

### The Generic Payload (LRM Chapter 14)

Interoperability in a virtual platform requires that all models agree on the data structure being transmitted. The LRM defines `tlm_generic_payload` (often referred to as `tlm::tlm_generic_payload` in the source).

#### Mandatory Attributes (LRM 14.8-14.16)
The standard dictates the precise semantics of the Generic Payload attributes:
1.  **Command (`m_command`):** `TLM_READ_COMMAND`, `TLM_WRITE_COMMAND`, or `TLM_IGNORE_COMMAND`.
2.  **Address (`m_address`):** A 64-bit integer (`sc_dt::uint64`).
3.  **Data Pointer (`m_data`):** A pointer to the payload bytes.
4.  **Data Length (`m_length`):** The number of bytes to read/write.
5.  **Byte Enables (`m_byte_enable`):** Allows for sparse transfers (e.g., writing only 2 bytes in a 4-byte word).
6.  **Streaming Width (`m_streaming_width`):** Defines burst behavior. If streaming width < data length, the transaction wraps back to the start address.
7.  **Response Status (`m_response_status`):** Initially `TLM_INCOMPLETE_RESPONSE`. Must be set by the target to `TLM_OK_RESPONSE`, `TLM_ADDRESS_ERROR_RESPONSE`, etc.
8.  **DMI Allowed (`m_dmi`):** A boolean flag indicating if Direct Memory Interface is possible.

#### Generic Payload Memory Management (LRM 14.4)
In a high-speed VP, allocating and deallocating `tlm_generic_payload` objects via `new`/`delete` for every transaction causes catastrophic performance degradation. The LRM requires the use of a memory manager (`tlm_mm_interface`). Initiators allocate payloads from a pool, attach the memory manager, and when the transaction's reference count drops to zero, the payload is returned to the pool without deallocation.

### Direct Memory Interface (DMI) (LRM Chapter 11.2)

For virtual platforms executing code from memory, even the fast `b_transport` is too slow because it requires a virtual function call and address decoding for *every single instruction fetch*.

DMI solves this.
1.  The initiator sends a normal `b_transport` with the `DMI Allowed` flag checked.
2.  The target (e.g., RAM) replies and sets the flag to true.
3.  The initiator calls `get_direct_mem_ptr()`.
4.  The bus translates the address, forwards the request, and returns a `tlm_dmi` object. This object contains a direct C++ raw pointer (`unsigned char* dmi_ptr`) to the host machine's memory, along with the start/end address range and read/write permissions.
5.  The initiator caches this pointer. Future reads/writes to that address range bypass the TLM sockets entirely and use direct C++ array indexing.

This single feature allows SystemC VPs to boot Linux in seconds rather than hours.

## Deep Dive: Accellera Source Code

### The Core Interfaces in `tlm_core`
If you explore the Accellera source code, you'll see the separation of the SystemC kernel (`sysc/`) from the TLM library (`src/tlm_core/`).

In `tlm_core/tlm_2/tlm_interfaces/tlm_fw_bw_ifs.h`, you see the pure C++ translation of the LRM:
```cpp
class tlm_fw_transport_if : public virtual sc_core::sc_interface {
public:
  virtual void b_transport(tlm_generic_payload& trans, sc_core::sc_time& t) = 0;
  virtual tlm_sync_enum nb_transport_fw(tlm_generic_payload& trans, tlm_phase& phase, sc_core::sc_time& t) = 0;
  virtual bool get_direct_mem_ptr(tlm_generic_payload& trans, tlm_dmi& dmi_data) = 0;
  virtual unsigned int transport_dbg(tlm_generic_payload& trans) = 0;
};
```

Notice that `b_transport` is not allowed to return a status enum; it must throw an exception if a hard failure occurs, but relies entirely on modifying the `response_status` of the payload object for transaction results.

### The Standard Sockets vs Utils
The standard defines `tlm_initiator_socket` and `tlm_target_socket` in `tlm_core/tlm_2/tlm_sockets/`. These are the raw binding points.

However, the examples and common patterns use `tlm_utils`.
In `tlm_core/tlm_2/tlm_utils/simple_target_socket.h`, the implementation essentially wraps a `tlm_target_socket` and provides the target implementation itself:

```cpp
// Conceptual simple_target_socket internal binding
template <typename MODULE, ...>
class simple_target_socket : public tlm_target_socket<...> {
   // It contains a private class that implements tlm_fw_transport_if
   class fw_process : public tlm_fw_transport_if {
       // ... implements b_transport by calling the user's registered callback
   };
   fw_process m_fw_process;

public:
   simple_target_socket() {
       // It binds its own base class socket to its internal implementation!
       this->bind(m_fw_process);
   }
};
```

This self-binding mechanism is what allows you to instantiate a `simple_target_socket`, register a callback, and never have to derive your module from `tlm_fw_transport_if`.

### Implementing the Interconnect (Bus)

In our Simple Bus example, the `b_transport` method manipulated the `trans.get_address()`. The LRM explicitly permits an interconnect to modify the address to route the transaction to a target (LRM 14.14).
However, **Rule 14.14(e)** strictly states that the interconnect MUST restore the address to its original value before returning control to the initiator. Failure to do so corrupts the generic payload for the initiator's subsequent use.

Furthermore, an interconnect handling DMI requests (`get_direct_mem_ptr`) must translate the addresses in the returned `tlm_dmi` struct. If a target returns a DMI range of `0x0000 - 0x0FFF`, and the target is mapped at `0x1000` on the bus, the bus must adjust the DMI struct's range to `0x1000 - 0x1FFF` before returning it to the CPU.

By adhering to these strict LRM contracts and leveraging the `tlm_utils` classes, SystemC Virtual Platforms can integrate CPU instruction set simulators (like QEMU or FastModels) with custom peripherals to achieve near real-time execution speeds.

## Lesson 31: Testing, Tracing, and Debugging SystemC Models

Canonical lesson: https://www.learn-systemc.com/tutorials/030-testing-tracing-and-debugging-systemc-models

How to test modules, generate VCD traces, debug TLM transactions, and make failures explain themselves.

## How to Read This Lesson

Treat this as engineering practice, not trivia. The patterns here are the ones that keep large models understandable after the original author has moved on.

SystemC models should be testable like software and observable like hardware. A good model does not merely run; it tells you what it did, when it did it, and why it rejected bad input.

## Standard and source context

Practice lessons should still cite their roots. Use `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for behavior, `Accellera SystemC GitHub repository/systemc` for the reference kernel, and `Accellera SystemC GitHub repository/systemc-common-practices` for reusable modeling patterns. The goal is to turn standard rules into habits that survive real project scale.

## End-to-End Tracing Example

Below is a complete, compilable example demonstrating structural testing, VCD tracing, and the use of `SC_REPORT_*` macros for effective debugging.

```cpp
#include <systemc>
#include <iomanip>

using namespace sc_core;

SC_MODULE(Counter) {
  sc_in<bool> clk{"clk"};
  sc_in<bool> rst{"rst"};
  sc_out<int> count{"count"};

  SC_CTOR(Counter) {
    SC_METHOD(tick);
    sensitive << clk.pos();
    dont_initialize();
  }

  void tick() {
    if (rst.read()) {
      count.write(0);
      SC_REPORT_INFO(name(), "Reset active, count cleared.");
    } else {
      int next_val = count.read() + 1;
      count.write(next_val);
      if (next_val > 10) {
        SC_REPORT_WARNING(name(), "Count exceeded expected maximum of 10.");
      }
    }
  }
};

int sc_main(int argc, char* argv[]) {
  sc_clock clk("clk", 10, SC_NS);
  sc_signal<bool> rst("rst");
  sc_signal<int> count("count");

  Counter dut("dut");
  dut.clk(clk);
  dut.rst(rst);
  dut.count(count);

  // 1. Setup VCD Tracing
  sc_trace_file* tf = sc_create_vcd_trace_file("wave");
  tf->set_time_unit(1, SC_NS);
  sc_trace(tf, clk, "clk");
  sc_trace(tf, rst, "rst");
  sc_trace(tf, count, "count");

  // 2. Executable Test Sequence
  rst.write(true);
  sc_start(25, SC_NS); // Hold reset

  rst.write(false);
  sc_start(100, SC_NS); // Run normal counting

  // 3. Automated State Check
  if (count.read() != 10) {
    SC_REPORT_ERROR("Testbench", "Counter did not reach expected value of 10.");
    sc_close_vcd_trace_file(tf);
    return 1;
  }

  sc_close_vcd_trace_file(tf);
  return 0;
}
```

## Executable Tests

Keep your tests deterministic. Avoid tests that depend on wall-clock time, random ordering, or host-specific output formatting. Small targeted models test reset behavior, FIFO occupancy, interrupt logic, or transaction status much faster than large integration tests.

## Transaction Tracing

For TLM platforms, transaction logs are often more useful than waveforms. A good transaction log includes the initiator name, target name, command, address, data length, byte-enable state, response status, annotated delay, and simulation timestamp.

```cpp
// Inside a b_transport callback:
std::ostringstream msg;
msg << "WRITE addr=0x" << std::hex << trans.get_address()
    << " len=" << std::dec << trans.get_data_length()
    << " delay=" << delay;
SC_REPORT_INFO(name(), msg.str().c_str());
```

## Debugging Delta Cycles

When a signal appears one step late, remember that signal writes are deferred until the update phase. To prove a delta-cycle issue, you can temporarily add diagnostic prints before and after `wait(SC_ZERO_TIME);`. However, do not fix delta-cycle bugs by scattering zero-time waits everywhere. First, understand which process writes, which channel updates, and which event wakes the reader according to the LRM scheduling phases.

## Failure Messages

The best failure message answers four questions:

1. Which model object failed?
2. What simulated time was it?
3. What operation was attempted?
4. What rule was violated?

Using `SC_REPORT_ERROR(name(), msg.str().c_str());` is much better than `assert(false)`. Assertions crash the simulator abruptly, while reports use the SystemC diagnostic system, allowing users to override actions, catch the error, or downgrade it to a warning.

## Under the Hood: How `sc_trace` Works
When you call `sc_trace(tf, signal, "name")`, SystemC uses the `sc_trace_file` API (e.g., `vcd_trace_file` in `sysc/tracing/sc_vcd_trace.cpp`).
You might wonder how the trace file knows the signal's value changed. SystemC does *not* fire a callback on every signal write. Instead, `sc_trace` stores a pointer to the variable's memory address.
At the very end of the delta cycle (after the update phase), if tracing is enabled, the kernel calls `sc_trace_file::cycle()`. The tracer iterates over all registered variable pointers, compares their current memory value against their previous cached value, and if different, writes a value change to the `.vcd` file.

## IEEE 1666-2023 LRM: Reports, Tracing, and Diagnostics

The SystemC standard provides a formalized diagnostic system (LRM Chapter 8) and tracing system (LRM Chapter 8.3). Understanding these formalisms allows you to build robust, production-quality simulation environments.

### The Report Handling System (LRM 8.2)

The `SC_REPORT_*` macros are not just glorified `std::cout` statements. They are entry points into a highly configurable diagnostic routing system.

#### Message Types and Severities (LRM 8.2.1)
The LRM defines four severity levels:
*   `SC_INFO`: Informational messages. Simulation continues.
*   `SC_WARNING`: Potential problems. Simulation continues.
*   `SC_ERROR`: Recoverable errors. By default, throws an exception or halts.
*   `SC_FATAL`: Unrecoverable errors. Simulation immediately aborts.

Every report has a **Message Type** (a string, like `"Testbench"` or `"TLM-2"`) and a **Severity**.

#### Actions (LRM 8.2.2)
When a report is generated, the kernel determines what to do based on the configured **Actions** (`sc_actions`). The standard actions are:
*   `SC_DO_NOTHING`
*   `SC_THROW`: Throws an `sc_report` C++ exception.
*   `SC_LOG`: Writes to the simulation log.
*   `SC_DISPLAY`: Prints to standard output.
*   `SC_CACHE_REPORT`: Saves the report so it can be retrieved via `sc_report_handler::get_cached_report()`.
*   `SC_INTERRUPT`: Drops the user into the debugger (via an architecture-specific interrupt trap).
*   `SC_STOP`: Calls `sc_stop()` to cleanly end the simulation.
*   `SC_ABORT`: Aborts the program immediately (e.g., via `abort()`).

#### The `sc_report_handler` (LRM 8.2.3)
The `sc_report_handler` is the static router for all diagnostics. You can configure it to change the default actions based on Severity, Message Type, or a combination of both.

For example, if a specific 3rd-party IP model generates too many `"TLM-2"` warnings, you can suppress them in your testbench:
```cpp
sc_report_handler::set_actions("TLM-2", SC_WARNING, SC_DO_NOTHING);
```
Or, you can force the simulator to drop into GDB whenever *any* error occurs:
```cpp
sc_report_handler::set_actions(SC_ERROR, SC_DISPLAY | SC_INTERRUPT);
```

You can even replace the entire report handler with your own custom C++ function (LRM 8.2.6) using `sc_report_handler::set_handler()`. This is incredibly useful for integrating SystemC logs into Python testing frameworks or standard corporate logging infrastructure (like Log4cxx).

### VCD Tracing Formalisms (LRM 8.3)

The standard explicitly defines the behavior of Value Change Dump (VCD) tracing.
*   **Trace Files (`sc_trace_file`):** Tracing is initialized by `sc_create_vcd_trace_file()`.
*   **Time Units:** You must explicitly set the timescale via `set_time_unit()`. If you do not, the LRM states it defaults to 1 picosecond (which might bloat your waveform file with excessive precision).
*   **Registration Rules:** Variables and signals must be registered with the trace file *before* simulation begins (i.e., before `sc_start()` is called). Attempting to call `sc_trace` while the simulation is running results in undefined behavior (and typically throws an error in the Accellera implementation).
*   **Delta vs Timed Tracing:** By default, SystemC trace files record values at the end of a time step (after all delta cycles have settled). However, `sc_trace_file::delta_cycles(true)` can be called to dump a state change for every single delta cycle. This is an advanced debug technique when tracking down combinational logic loops.

### Phase Callbacks for Introspection (LRM 4.1.4, 4.3.4)

When debugging, sometimes you need to walk the module hierarchy or inject code right before simulation starts. The LRM defines phase callbacks that every `sc_module` and `sc_channel` can override:

*   `void before_end_of_elaboration()`
*   `void end_of_elaboration()`: All ports are bound. You can traverse the hierarchy here.
*   `void start_of_simulation()`: Called right before the Initialization Phase. Excellent for opening files, initializing external debug sockets, or setting up initial state.
*   `void end_of_simulation()`: Called when `sc_stop()` finishes. Used for closing files, flushing trace buffers, and printing final statistics.

## Deep Dive: Accellera Source Code

### The Tracing Implementation (`sysc/tracing`)

If you look at `sysc/tracing/sc_vcd_trace.cpp`, you'll see how `sc_trace` actually captures C++ variables without the variables knowing they are being traced.

When you call `sc_trace(tf, my_int, "my_int")`, the trace file dynamically allocates a subclass of `sc_trace_file_base::vcd_trace`. For integers, it creates a `vcd_T_trace<int>`.
This object stores:
1.  A `const int*` pointer to the actual memory address of `my_int`.
2.  An `int` representing the "previous value".

The SystemC kernel holds a list of all active trace files. At the end of `sc_simcontext::crunch()` (the simulation loop), if time has advanced, it calls `tf->cycle()`.
The `cycle()` function loops over every registered `vcd_T_trace`. It dereferences the pointer to read the current memory value, compares it to the previous value, and if it has changed, it writes the string representation (e.g., binary `1010`) to the `.vcd` file.

This means you can trace *any* vanilla C++ variable in your module, not just `sc_signal`s. Just remember that the trace mechanism only observes the value at the *end* of the evaluation phase; it does not intercept assignments.

### The Error Handler Implementation (`sysc/utils/sc_report_handler.cpp`)

The Accellera report handler uses a set of highly optimized internal maps to route messages.
When you invoke `SC_REPORT_INFO()`, the macro captures `__FILE__` and `__LINE__` and forwards them to `sc_report_handler::report()`.

The implementation checks the routing rules in this order:
1.  Is there a rule for this specific `(Message Type, Severity)`?
2.  If not, is there a rule for this specific `Severity`?
3.  If not, is there a rule for this specific `Message Type`?
4.  If not, use the default action for that `Severity`.

If the resulting action contains `SC_THROW`, the kernel instantiates an `sc_report` object and throws it as a C++ exception (`throw report;`). This is why you must never catch generic exceptions (`catch (...)`) around `sc_start()`; doing so intercepts SystemC's internal error handling and can lead to corrupted simulation states.

Understanding these mechanisms allows you to write testbenches that gracefully catch specific errors for negative testing (verifying that a model *does* throw an error on bad input), ensuring your models are both rigorous and compliant.

## Lesson 32: Reports, Errors, and Debugging

Canonical lesson: https://www.learn-systemc.com/tutorials/031-reports-errors-and-debugging

Using SC_REPORT_INFO, warnings, errors, hierarchy names, assertions, and debug patterns.

## How to Read This Lesson

Treat this as engineering practice, not trivia. The patterns here are the ones that keep large models understandable after the original author has moved on.

Debuggability is part of the model. A virtual platform that fails with a vague C++ exception is harder to trust than one that reports the module, transaction, address, and simulated time. The IEEE 1666 LRM provides a comprehensive `sc_report_handler` utility for exactly this purpose.

## Standard and source context

Practice lessons should still cite their roots. Use `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for behavior, `Accellera SystemC GitHub repository/systemc` for the reference kernel, and `Accellera SystemC GitHub repository/systemc-common-practices` for reusable modeling patterns. The goal is to turn standard rules into habits that survive real project scale.

## End-to-End Reporting Example

Here is a full compilable example demonstrating `SC_REPORT_INFO`, `SC_REPORT_WARNING`, `SC_REPORT_ERROR`, named object tracking, and even configuring the global report handler to stop simulation on specific errors.

```cpp
#include <systemc>
#include <iomanip>
#include <sstream>

using namespace sc_core;

SC_MODULE(UartModel) {
  SC_CTOR(UartModel) {
    SC_THREAD(run);
  }

  void run() {
    // 1. Basic Info
    SC_REPORT_INFO(name(), "UART Initialization Complete");
    wait(10, SC_NS);

    // 2. Warning with context
    SC_REPORT_WARNING(name(), "Buffer nearing capacity!");
    wait(10, SC_NS);

    // 3. Formatted Error using string stream
    uint32_t bad_address = 0xFFFFFFFF;
    std::ostringstream msg;
    msg << "Access to unmapped address 0x"
        << std::hex << bad_address
        << " at time " << sc_time_stamp();

    SC_REPORT_ERROR("AddressDecoder", msg.str().c_str());
  }
};

int sc_main(int argc, char* argv[]) {
  // Configure the report handler to abort simulation on the first error
  sc_report_handler::set_actions(SC_ERROR, SC_DISPLAY | SC_ABORT);

  UartModel uart("uart");
  sc_start(100, SC_NS);

  return 0;
}
```

## SystemC Reports

SystemC provides standard report macros. Reports include severity (INFO, WARNING, ERROR, FATAL) and can include source location depending on configuration.

Use categories (the first argument to `SC_REPORT_INFO`) consistently so users can filter logs via `sc_report_handler::set_actions()`.

## Include Time and Path

Named hierarchy is one of SystemC's strengths. Use it. `name()` gives the hierarchical object name (e.g. `top.bus.uart`). This makes messages actionable in large systems.

## Assertions

Prefer explicit `SC_REPORT_ERROR` or `SC_REPORT_FATAL` for model/user mistakes because they can carry context, time, and hierarchy strings. Plain `assert()` is acceptable for internal developer invariants, but it crashes the simulator abruptly, preventing graceful log flushes or user-defined intercepts.

## Transaction Debugging

For TLM, log the complete transaction shape. This reveals whether a bug is in address decode, target behavior, timing, byte enables, or response propagation.

## Delta-Cycle Debugging

If a signal-based model behaves one event late, print both `sc_time_stamp()` and relevant values before and after `wait(SC_ZERO_TIME)`. Do not leave noisy debug logs enabled by default. Add a verbosity flag or compile-time switch.

## Under the Hood: `sc_report_handler`
The macro `SC_REPORT_INFO(msg_type, msg)` expands into a call to `sc_core::sc_report_handler::report()`.
In `sysc/utils/sc_report_handler.cpp`, the kernel maintains a registry of message types. When a report is triggered, it checks the configured severity (`SC_INFO`, `SC_WARNING`, `SC_ERROR`, `SC_FATAL`) and looks up the action (e.g., `SC_LOG`, `SC_DISPLAY`, `SC_STOP`, `SC_ABORT`).
If the action is `SC_ABORT`, it calls `std::abort()`. If it is `SC_THROW`, it throws an `sc_report` exception. You can completely customize this behavior by overriding the report handler, directing logs to a custom GUI, a file, or a network socket.

## Lesson 33: LRM Roadmap: How the Standards Fit Together

Canonical lesson: https://www.learn-systemc.com/tutorials/032-lrm-roadmap-how-the-standards-fit-together

A learning-oriented map of the SystemC, AMS, CCI, and UVM-SystemC LRMs in this repository.

## How to Read This Lesson

This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.

This site uses the LRMs in `Docs/LRMs` as reference sources, but the public lessons are written as explanations. The goal is to help you read the standards without feeling like you were dropped into an index. Furthermore, we dive into the **Accellera C++ source code implementations** to show you exactly how these standards are built under the hood.

The repository includes these reference manuals:

- **IEEE 1666-2023 SystemC Language Reference Manual:** The core simulation kernel, events, and TLM standard.
- **SystemC AMS 2.0 Language Reference Manual:** Analog and Mixed-Signal extensions.
- **SystemC CCI 1.0 Language Reference Manual:** Configuration, Control, and Inspection API.
- **UVM-SystemC Language Reference Manual:** Universal Verification Methodology for SystemC.

## Standard and source context

## How to Think About the Stack

SystemC 1666 is the base language and simulation kernel. It defines modules, processes, events, time, ports, exports, channels, datatypes, reports, tracing, and the Transaction Level Modeling (TLM) 2.0 standard. All other LRMs build on this discrete-event foundation.
- **Under the Hood:** Everything in SystemC derives from `sc_core::sc_object`. This creates a hierarchical object tree managed by `sc_simcontext`. The discrete-event scheduler (`sc_simcontext::crunch()`) is the heartbeat of all other standards.

SystemC AMS adds analog/mixed-signal modeling styles on top of SystemC. Its central ideas are timed data flow (TDF), conservative electrical networks, and linear signal flow (LSF), solving analog equations in conjunction with the discrete-event solver.
- **Under the Hood:** AMS introduces a synchronization layer (`sca_core::sca_implementation::sca_sync_obj`). The TDF solver registers itself as an `sc_module` and schedules equation evaluations using `sc_core::next_trigger(sc_time)` or delta delays, effectively bridging continuous time matrices to the discrete-event `m_timed_events` queue.

SystemC CCI adds configuration, control, and inspection. It provides standard APIs to expose parameters (`cci_param`), broker access, and track where configuration values originated, heavily utilized in Virtual Platforms.
- **Under the Hood:** CCI relies heavily on the underlying `sc_object` hierarchy string names (`name()`). The `cci_broker_if` interacts with a global map of parameters. When a parameter is accessed or mutated, CCI utilizes `cci_value` (which is effectively a JSON-like AST implemented using C++ `union` and `std::string` mappings) to serialize/deserialize typed data across IPs.

UVM-SystemC adds verification methodology: components, factory, phases, sequences, sequencers, configuration, reporting, TLM ports, and register abstractions, porting SystemVerilog UVM to C++.
- **Under the Hood:** `uvm_component` inherits from `sc_module`. The UVM phasing mechanism leverages SystemC's `sc_spawn` to create dynamic threads that wait on phase barriers (`sc_event`). The factory pattern uses static initialization and C++ RTTI (`typeid` or macro-based registration) to instantiate objects by string names at runtime.

## What "Between Learning Site and LRM" Means

An LRM strictly dictates legal syntax, elaboration phases, memory management, and standard compliance. It tells you what is defined. A learning site tells you how to think. These lessons aim to do both:

- Name the LRM area explicitly.
- Explain why it exists in the hardware modeling ecosystem.
- Expose the exact **Accellera GitHub repository** C++ implementations (`systemc`, `cci`, `uvm-systemc`, `systemc-ams`).
- Provide exhaustive, 100% compilable, end-to-end `sc_main` modeling patterns.
- Warn about common C++ and SystemC mistakes.

If a lesson references standard definitions, treat it as exhaustive technical guidance compliant with the IEEE specs. Every code snippet provided in these LRM chapters is a fully independent model you can compile and run directly to observe the LRM's behavior firsthand.

## Reading Order

If you are new, start with chapters 1 through 6. Then read the LRM bridge chapters:

1. **SystemC 1666 Semantics and Core Classes:** Elaboration, execution phases, and process macros.
2. **Ports, Exports, Channels, Datatypes, and TLM:** The structural and transaction-level backbone.
3. **AMS Models of Computation:** Timed Data Flow and solver synchronization.
4. **CCI Parameters and Brokers:** Tool-independent IP configuration.
5. **UVM-SystemC Verification Architecture:** Testbenches and constrained randomization.

That order ensures the LRMs become a structured technical guide rather than an overwhelming reference index.

## Lesson 34: LRM Bridge: Elaboration and Simulation Semantics

Canonical lesson: https://www.learn-systemc.com/tutorials/033-lrm-bridge-elaboration-and-simulation-semantics

The SystemC 1666-2023 model of construction, elaboration, initialization, evaluation, update, delta notification, and timed notification.

## How to Read This Lesson

This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.

The IEEE 1666 LRM strictly separates a model's life cycle into **elaboration** and **simulation**. This distinction is the key to understanding many API rules and avoiding illegal dynamic structural changes. To fully appreciate this, we must look inside the **Accellera SystemC kernel**.

## Standard and source context

## Elaboration

Elaboration is where the structural topology becomes known. Modules are constructed, child objects are named, ports and exports are bound, process macros register behavior, and time resolution is set.

**Under the Hood:**
The global `sc_simcontext` singleton maintains the simulation state. During elaboration, creating an `sc_module` triggers `sc_module::sc_module()`, which registers the module into `sc_simcontext::m_object_manager` forming the `sc_object` hierarchy. Port bindings (`port(channel)`) push binding requests into a deferred queue (`m_bind_info`).
You are executing standard C++ constructors, but the SystemC kernel is simultaneously observing and registering the hierarchy. Binding belongs entirely in this phase. Once `sc_start()` is called, the kernel executes `sc_simcontext::elaborate()`, iterating over the binding queues to resolve `sc_port` to `sc_interface` pointers. Trying to alter topology after this causes an `SC_FATAL`.

## Simulation and the Discrete-Event Scheduler

Simulation begins when control enters the scheduler, normally through `sc_core::sc_start()`. The LRM describes scheduling through explicit, strictly ordered phases.
In the source code, this is orchestrated by `sc_simcontext::initialize()` followed by the central `sc_simcontext::crunch()` and `sc_simcontext::next_time()` loop.

1. **Initialization (`initialize()`):** Processes are made runnable. The kernel iterates over all process handles (`sc_method_handle`, `sc_thread_handle`) and pushes them into the runnable queues (`m_runnable->push_back()`), unless `dont_initialize()` was called.
2. **Evaluation (`crunch()` part 1):** Runnable processes pop from the queue and execute (using `semantics()` for methods, or `suspend_me()`/`coroutine_resume` for threads) until they yield (`wait()`) or finish.
3. **Update (`crunch()` part 2):** Channels commit pending values. The kernel iterates over `m_update_list` (a vector of `sc_update_if*`) and calls `update()` on each channel (e.g., `sc_signal::update()`).
4. **Delta Notification (`crunch()` part 3):** Events with zero-time delay are triggered. `m_delta_events` are processed, which may push new processes into `m_runnable`. If `m_runnable` is not empty, loop back to Evaluation (Delta Cycle).
5. **Timed Notification (`next_time()`):** When `m_runnable` and `m_delta_events` are empty, `sc_simcontext` peeks at `m_timed_events` (a standard C++ priority queue sorted by timestamp). It pops the nearest events, updates `m_curr_time`, schedules the associated processes into `m_runnable`, and loops back to `crunch()`.

## End-to-End LRM Example

Here is a complete example demonstrates elaboration, initialization, delta cycles, and timed notification, strictly following IEEE 1666 semantics.

```cpp
#include <systemc>

// A module demonstrating initialization and evaluation phases
SC_MODULE(SemanticsDemo) {
    sc_core::sc_signal<bool> ready{"ready"};
    sc_core::sc_event timed_event;

    SC_CTOR(SemanticsDemo) {
        // Registered during elaboration
        SC_METHOD(initialization_method);
        // By default, methods are placed in the runnable queue during initialization.
        // We do not call dont_initialize() here.

        SC_THREAD(simulation_thread);
        // Wait for the ready signal to go true
        sensitive << ready.pos();
    }

    // This method runs automatically at time 0 (Initialization Phase)
    void initialization_method() {
        std::cout << "@" << sc_core::sc_time_stamp()
                  << " [Init/Eval Phase] Method executing." << std::endl;

        // Write a value. This schedules an Update request, it does NOT change the value instantly.
        ready.write(true);

        // Schedule a timed event for 10 ns in the future
        timed_event.notify(10, sc_core::SC_NS);

        // Prevent it from re-running endlessly
        next_trigger(timed_event);
    }

    void simulation_thread() {
        while(true) {
            // Wakes up when ready becomes true (which happens in the Delta Update phase)
            wait();
            std::cout << "@" << sc_core::sc_time_stamp()
                      << " [Eval Phase] Thread woke up from ready.pos() delta event." << std::endl;

            // Suspend until the timed event fires
            wait(timed_event);
            std::cout << "@" << sc_core::sc_time_stamp()
                      << " [Timed Notification] Thread woke up from timed_event." << std::endl;
        }
    }
};

int sc_main(int argc, char* argv[]) {
    // Elaboration Phase
    SemanticsDemo demo("demo");

    std::cout << "--- Starting Simulation ---" << std::endl;
    // Simulation Phase begins
    sc_core::sc_start(20, sc_core::SC_NS);
    std::cout << "--- Simulation Finished ---" << std::endl;

    return 0;
}
```

## Initialization Rules

Before time advances, some processes are placed in the runnable queue. That is why `SC_METHOD` processes often run at time zero. Under the hood, `dont_initialize()` sets a boolean flag `m_dont_init` in the `sc_process_b` base class, which `sc_simcontext::initialize()` reads to skip pushing it into `m_runnable`.

For clocked behaviors, you almost always want to prevent this initial time-zero execution:

```cpp
SC_METHOD(tick);
sensitive << clk.pos();
dont_initialize(); // Crucial to prevent time-zero glitching
```

Without `dont_initialize()`, your "clocked" behavior will execute before the very first clock edge actually occurs.

## Delta Cycles vs Timed Notification

Delta cycles let the model settle without advancing physical simulation time. A delta cycle can propagate events from one process to another while `sc_time_stamp()` remains strictly unchanged. This is not an implementation trick; it is a fundamental pillar of hardware description languages to model concurrency on parallel wires.

Timed Notification occurs only when the delta-event queue is empty. The scheduler jumps time forward to the nearest pending timed event in the `m_timed_events` priority queue.

## Practical Questions to Ask

When debugging LRM-compliant models:
- Is this code executing during a constructor (elaboration) or during `sc_start` (simulation)?
- Did the process unintentionally initialize at time zero because `dont_initialize()` was forgotten?
- Is the read value pending an update (in `m_update_list`), or has the update phase already committed it?
- Did the event notify immediately (`notify()`), in a delta (`notify(SC_ZERO_TIME)`), or at a future time (`notify(time)`)?

## Source-reading checkpoint

When elaboration order looks surprising, inspect the Accellera `sc_module` and `sc_simcontext` paths. That is where constructed objects become kernel-visible hierarchy before simulation starts. For binding-related elaboration, continue into `sc_port_registry`.

## Lesson 35: LRM Bridge: Core Classes and Process Control

Canonical lesson: https://www.learn-systemc.com/tutorials/034-lrm-bridge-core-classes-and-process-control

sc_module, sc_module_name, sc_spawn, sc_process_handle, reset, sensitivity, wait, next_trigger, and process control.

## How to Read This Lesson

This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.

The SystemC core classes define how structural models are instantiated and how behavioral processes become schedulable and controllable. Here we dive into the Accellera kernel source code to show how the hierarchy and scheduler control these primitives.

## Standard and source context

## sc_module and sc_module_name

`sc_module` gives a model its structural hierarchy. The class `sc_module_name` must be the first parameter in the constructor to support string-based naming during elaboration.

**Under the Hood:**
`sc_module` derives from `sc_object`. When you instantiate an `sc_module_name` variable, its constructor pushes itself onto a global stack managed by `sc_simcontext`. When `sc_module` is constructed immediately after, it pops that name off the stack and registers the instance as a child of the currently active `sc_module` scope. This creates the hierarchical object tree `sc_simcontext::m_object_manager`, which is the backbone of hierarchical trace names, CCI paths, and UVM-SystemC factory lookups.

## Process Macros

The IEEE 1666 standard defines three main process macros.
**Under the Hood:** They all expand into standard C++ member functions registered via a helper class (e.g., `sc_process_b` hierarchy).
- `SC_METHOD`: Creates an `sc_method_process` which executes to completion via `semantics()` without suspension.
- `SC_THREAD`: Creates an `sc_thread_process` which allocates a coroutine stack (via `qt_allocate` or `makecontext`) and can suspend using `wait()`.
- `SC_CTHREAD`: A specialized thread (`sc_cthread_process`) sensitive to a single clock edge, often used for High-Level Synthesis (HLS).

## Complete Process Control & Sensitivity Example

This end-to-end example demonstrates static sensitivity, dynamic sensitivity (`next_trigger`), reset signaling, and dynamic process control via `sc_process_handle`.

```cpp
#include <systemc>

SC_MODULE(ProcessControlDemo) {
    sc_core::sc_in<bool> clk;
    sc_core::sc_signal<bool> reset{"reset"};
    sc_core::sc_event dynamic_event;

    // Process handle to control a thread externally
    sc_core::sc_process_handle thread_handle;

    SC_CTOR(ProcessControlDemo) {
        // 1. SC_METHOD with dynamic sensitivity
        SC_METHOD(one_shot_method);
        dont_initialize();

        // 2. SC_THREAD with static sensitivity and asynchronous reset
        SC_THREAD(worker_thread);
        sensitive << clk.pos();
        async_reset_signal_is(reset, true); // Reset is active-high

        // Capture the handle of the most recently created process (worker_thread)
        thread_handle = sc_core::sc_get_current_process_handle();

        // 3. SC_THREAD to control the worker and trigger events
        SC_THREAD(controller_thread);
    }

    void one_shot_method() {
        std::cout << "@" << sc_core::sc_time_stamp()
                  << " [Method] Fired due to dynamic_event." << std::endl;

        // Dynamic sensitivity: trigger again ONLY on the next occurrence
        next_trigger(dynamic_event);
    }

    void worker_thread() {
        while(true) {
            // Check for reset condition
            if (sc_core::sc_process_handle::is_unwinding()) {
                std::cout << "@" << sc_core::sc_time_stamp()
                          << " [Worker] Thread reset triggered!" << std::endl;
                // Perform reset logic here (clear queues, reset state machines)
                wait(); // Suspend and wait for reset to clear
            }

            wait(); // Wait for static sensitivity (clk.pos)
            std::cout << "@" << sc_core::sc_time_stamp()
                      << " [Worker] Doing work..." << std::endl;
        }
    }

    void controller_thread() {
        wait(15, sc_core::SC_NS);
        std::cout << "@" << sc_core::sc_time_stamp() << " [Controller] Triggering method." << std::endl;
        dynamic_event.notify();

        wait(20, sc_core::SC_NS);
        std::cout << "@" << sc_core::sc_time_stamp() << " [Controller] Asserting reset." << std::endl;
        reset.write(true);

        wait(20, sc_core::SC_NS);
        std::cout << "@" << sc_core::sc_time_stamp() << " [Controller] De-asserting reset." << std::endl;
        reset.write(false);

        wait(20, sc_core::SC_NS);
        std::cout << "@" << sc_core::sc_time_stamp() << " [Controller] Suspending worker via handle." << std::endl;
        thread_handle.suspend();

        wait(20, sc_core::SC_NS);
        std::cout << "@" << sc_core::sc_time_stamp() << " [Controller] Resuming worker via handle." << std::endl;
        thread_handle.resume();
    }
};

int sc_main(int argc, char* argv[]) {
    sc_core::sc_clock clk("clk", 10, sc_core::SC_NS);
    ProcessControlDemo demo("demo");
    demo.clk(clk);

    sc_core::sc_start(120, sc_core::SC_NS);
    return 0;
}
```

## Sensitivity

**Static sensitivity** connects a process to events before simulation starts using `sensitive <<`. The kernel maintains a static list of events (`sc_event_list`) within `sc_process_b`.
**Dynamic sensitivity** is expressed by `wait()` in threads or `next_trigger()` in methods, which temporarily overrides the static list for the next scheduler wakeup by placing an event in `m_trigger_event`.

Use static sensitivity for stable, physical hardware structures (like a clock pin on a D-Flip-Flop). Use dynamic sensitivity when modeling software, TLM state machines, or protocols where the next wakeup event heavily depends on the current runtime state.

## Process Handles

`sc_process_handle` gives programmatic access to a process object (which wraps `sc_process_b*`). Depending on the simulation phase and process state, a model can `suspend()`, `resume()`, `disable()`, `enable()`, `kill()`, or sync with a process.

**Under the Hood:** Calling `suspend()` sets `m_state = ps_suspended` on the underlying `sc_process_b` and removes the process from the `m_runnable` scheduler queue if it was pending.

## Reset

SystemC provides standard reset semantics via `reset_signal_is` (synchronous) and `async_reset_signal_is` (asynchronous).

**Under the Hood:** When an asynchronous reset signal asserts, the scheduler calls `trigger_reset()` on the `sc_process_b`. If the thread is executing, it literally throws a C++ exception: `throw sc_unwind_exception()`. The process thread stack unwinds until it hits the kernel's catch block, re-evaluates `is_unwinding()`, and resets its execution pointer to the start of the while loop.

## sc_spawn

`sc_core::sc_spawn` allows dynamic process creation at runtime.

**Under the Hood:** `sc_spawn` creates an `sc_spawn_options` object, packages the functor/lambda, and instantiates an `sc_thread_process` or `sc_method_process` dynamically using `sc_simcontext::create_thread_process()`. While permitted by the LRM, dynamically spawning threads during simulation breaks static elaboration, making debugging and static analysis tools (like structural visualizers) fail. Use `sc_spawn` predominantly for testbenches and UVM sequence generation, not for hardware RTL modeling.

## Source-reading checkpoint

For process-control debugging, follow the Accellera `sc_process` family from registration to runnable state changes. Keep the LRM rule separate from private scheduler bookkeeping. When a process depends on a bound interface, keep `sc_port_registry` in the same trace.

## Lesson 36: LRM Bridge: Events, Time, and Scheduler APIs

Canonical lesson: https://www.learn-systemc.com/tutorials/035-lrm-bridge-events-time-and-scheduler-apis

sc_event, event lists, sc_time, time resolution, sc_start, sc_pause, sc_stop, simulation status, and pending activity.

## How to Read This Lesson

This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.

Events and time form the primary vocabulary for inter-process communication in the SystemC discrete-event scheduler. Here we explore the IEEE 1666 rules and examine the **Accellera kernel C++ source code** to see how they are executed under the hood.

## Standard and source context

## sc_event

An `sc_core::sc_event` is a fundamental synchronization object. Processes can wait on it, and other processes or channels can notify it. Unlike software message queues, events do not queue or hold data. If a process is not actively waiting on an event when it is notified, that notification is permanently missed.

**Under the Hood (Accellera Kernel):**
When a process calls `wait(sc_event)`, the scheduler maps the `sc_process_b` handle to the `sc_event`'s internal dependency list. The LRM defines three notification types, and the `sc_event::notify()` method routes them through `sc_simcontext`:
1. **Immediate (`notify()`):** The event triggers in the current evaluation phase. The kernel instantly takes all suspended processes waiting on this event and pushes them onto the `sc_simcontext::m_runnable` queue.
2. **Delta (`notify(SC_ZERO_TIME)`):** The event triggers in the very next delta cycle. The event is pushed into `sc_simcontext::m_delta_events`.
3. **Timed (`notify(10, SC_NS)`):** The event triggers at a future simulation time. The event and its activation time are packaged and pushed into the `sc_simcontext::m_timed_events` priority queue.

## Event Lists (AND/OR semantics)

The IEEE 1666 standard provides event-list objects to allow processes to wake up on multiple, complex conditions.
- `wait(a | b)`: Wakes up if event `a` OR event `b` triggers.
- `wait(a & b)`: Suspends until BOTH event `a` AND event `b` have triggered.

**Under the Hood:**
These operators dynamically construct objects of type `sc_event_or_list` and `sc_event_and_list`. For an AND list, the kernel tracks an internal counter of how many events have fired. When an event fires, it decrements the counter. The process is only moved back to `m_runnable` when the counter reaches zero.

## Time and Simulation Control API

`sc_core::sc_time` represents absolute simulation time, distinct from physical wall-clock time.

**Under the Hood:** `sc_time` does not use floating-point math internally for simulation time because of precision loss. It stores time as an unsigned 64-bit integer (`sc_dt::uint64` representing a multiple of the global time resolution). You must define the time resolution (e.g., `sc_set_time_resolution(1, SC_PS)`) before the first `sc_time` object is constructed, otherwise the kernel will throw a fatal error.

The kernel can be controlled dynamically using:
- `sc_start(sc_time)`: Runs the simulation until the specified time elapses. It loops `crunch()` and `next_time()` inside `sc_simcontext`.
- `sc_pause()`: Pauses the simulation and returns control to `sc_main()`. It flags `m_simulation_status = SC_PAUSED` causing `sc_start`'s internal loop to break early.
- `sc_stop()`: Permanently halts the simulation. Once stopped (`SC_STOPPED`), you cannot call `sc_start` again.

## End-to-End Scheduler API Example

This fully compliant `sc_main` example demonstrates event lists, immediate vs. timed notification, time resolution, and stepping the simulation via `sc_start()`.

```cpp
#include <systemc>

SC_MODULE(SchedulerAPI_Demo) {
    sc_core::sc_event ev_a;
    sc_core::sc_event ev_b;

    SC_CTOR(SchedulerAPI_Demo) {
        SC_THREAD(consumer_thread);
        SC_THREAD(producer_thread);
    }

    void consumer_thread() {
        while(true) {
            std::cout << "@" << sc_core::sc_time_stamp()
                      << " delta=" << sc_core::sc_delta_count()
                      << " [Consumer] Waiting for Event A AND Event B..." << std::endl;

            // AND event list: Will wake up only after BOTH have fired.
            wait(ev_a & ev_b);

            std::cout << "@" << sc_core::sc_time_stamp()
                      << " delta=" << sc_core::sc_delta_count()
                      << " [Consumer] Woke up! Both events received." << std::endl;
        }
    }

    void producer_thread() {
        wait(10, sc_core::SC_NS);
        std::cout << "@" << sc_core::sc_time_stamp() << " [Producer] Notifying Event A (Immediate)" << std::endl;
        ev_a.notify(); // Immediate notification

        wait(5, sc_core::SC_NS);
        std::cout << "@" << sc_core::sc_time_stamp() << " [Producer] Notifying Event B (Delta)" << std::endl;
        ev_b.notify(sc_core::SC_ZERO_TIME); // Delta notification

        wait(20, sc_core::SC_NS);
        std::cout << "@" << sc_core::sc_time_stamp() << " [Producer] Pausing Simulation." << std::endl;
        sc_core::sc_pause();
    }
};

int sc_main(int argc, char* argv[]) {
    // Must be called before any time objects are constructed
    sc_core::sc_set_time_resolution(1, sc_core::SC_PS);

    SchedulerAPI_Demo demo("demo");

    std::cout << "--- Starting Simulation for 100 ns ---" << std::endl;
    // We can step the simulation in chunks
    sc_core::sc_start(20, sc_core::SC_NS);

    std::cout << "--- Back in sc_main. Checking status ---" << std::endl;
    if (sc_core::sc_get_status() == sc_core::SC_PAUSED) {
        std::cout << "Simulation was paused. Resuming..." << std::endl;
        sc_core::sc_start(); // Continue indefinitely until sc_stop() or exhaustion
    }

    return 0;
}
```

## Debugging Time Bugs

When diagnosing simulation hangs or zero-time loops, always print both the absolute time and the delta cycle count:

```cpp
std::cout << sc_core::sc_time_stamp()
          << " delta=" << sc_core::sc_delta_count() << std::endl;
```

If `sc_time_stamp()` is constant but `sc_delta_count()` increments infinitely, you have an un-clocked combinatorial feedback loop (an infinite zero-time scheduling loop). Under the hood, `sc_simcontext::crunch()` is looping endlessly over `m_delta_events` without ever returning to `next_time()`, and you must insert a timed `wait()` to break it.

## Lesson 37: LRM Bridge: Ports, Exports, Interfaces, and Channels

Canonical lesson: https://www.learn-systemc.com/tutorials/036-lrm-bridge-ports-exports-interfaces-and-channels

The standard communication model: sc_interface, sc_port, sc_export, sc_prim_channel, sc_channel, and binding policies.

## How to Read This Lesson

This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.

SystemC hardware communication is strictly interface-based, utilizing a separation of concerns defined by the LRM. Let's dig into the IEEE 1666 rules and the **Accellera C++ source code** to see how ports actually resolve into pointers.

## Standard and source context

## The Four Pillars of Communication

1. **`sc_interface`:** An abstract C++ class defining *what* can be done (e.g., `read()`, `write()`). Under the hood, it registers itself directly with the simulation context to enable type-safe dynamic casting and binding during elaboration.
2. **Channel (`sc_channel` or `sc_prim_channel`):** A module that *implements* the interface. It contains the actual state and logic.
3. **`sc_port`:** An outward-facing connection on a module that *requires* an interface to function. Under the hood, `sc_port` is just a proxy object that internally holds an array of `sc_interface*` pointers.
4. **`sc_export`:** An inward-facing connection on a module boundary that *provides* an interface implemented by a child module. It acts as a reverse proxy, delegating incoming bindings down to the internal `sc_interface`.

## End-to-End Interface and Channel Example

This fully compliant IEEE 1666 example demonstrates defining an interface, implementing a hierarchical channel, and binding a port to it.

```cpp
#include <systemc>

// 1. Define the Interface (inheriting from sc_interface)
struct RegisterIf : virtual public sc_core::sc_interface {
    virtual uint32_t read(uint32_t offset) = 0;
    virtual void write(uint32_t offset, uint32_t data) = 0;
};

// 2. Implement the Channel (inheriting from sc_channel and the interface)
class RegisterBank : public sc_core::sc_channel, public RegisterIf {
private:
    uint32_t memory[256];

public:
    SC_HAS_PROCESS(RegisterBank);
    RegisterBank(sc_core::sc_module_name name) : sc_core::sc_channel(name) {
        for (int i = 0; i < 256; i++) memory[i] = 0;
    }

    uint32_t read(uint32_t offset) override {
        if (offset < 256) {
            std::cout << "@" << sc_core::sc_time_stamp() << " [Bank] Read 0x"
                      << std::hex << memory[offset] << " from " << offset << std::endl;
            return memory[offset];
        }
        return 0;
    }

    void write(uint32_t offset, uint32_t data) override {
        if (offset < 256) {
            std::cout << "@" << sc_core::sc_time_stamp() << " [Bank] Wrote 0x"
                      << std::hex << data << " to " << offset << std::endl;
            memory[offset] = data;
        }
    }
};

// 3. Define a Module requiring the interface via a Port
SC_MODULE(CPU_Model) {
    // Requires a RegisterIf implementation
    sc_core::sc_port<RegisterIf> regs{"regs"};

    SC_CTOR(CPU_Model) {
        SC_THREAD(execute_logic);
    }

    void execute_logic() {
        wait(10, sc_core::SC_NS);
        regs->write(0x10, 0xDEADBEEF); // Accesses the channel via the port interface

        wait(10, sc_core::SC_NS);
        uint32_t val = regs->read(0x10);
    }
};

// 4. Encapsulate with an Export
SC_MODULE(Subsystem) {
    // Exposes the internal RegisterBank to the outside world
    sc_core::sc_export<RegisterIf> target_export{"target_export"};
    RegisterBank regs{"regs"};

    SC_CTOR(Subsystem) {
        // Bind the export to the internal channel
        target_export.bind(regs);
    }
};

int sc_main(int argc, char* argv[]) {
    CPU_Model cpu("cpu");
    Subsystem subsys("subsys");

    // Bind the CPU's port to the Subsystem's export
    cpu.regs.bind(subsys.target_export);

    sc_core::sc_start(50, sc_core::SC_NS);
    return 0;
}
```

## Primitive vs Hierarchical Channels and the Kernel

- **Primitive Channels (`sc_prim_channel`):** Used for fundamental data types (like `sc_signal`). **Under the hood**, they hook directly into the Evaluate-Update paradigm. When you write to a signal, `sc_prim_channel::request_update()` pushes the `this` pointer into `sc_simcontext::m_update_list`. Later, during the Update Phase, the scheduler loops over this list and calls the purely virtual `update()` method. They cannot have structural hierarchy (child modules) or `SC_THREAD`s.
- **Hierarchical Channels (`sc_channel`):** Modules that implement an interface (as shown in the example above). They derive directly from `sc_module` and can contain internal processes (`SC_THREAD`), ports, and child modules. They do not automatically hook into the delta-cycle update list.

## Binding Policies

The LRM enforces strict binding constraints during the elaboration phase.

**Under the Hood:** When you call `cpu.regs.bind(...)`, the port does not resolve the pointer immediately. It stores a `sc_bind_info` struct in a deferred queue. When `sc_start()` is called, `sc_simcontext::elaborate()` resolves all these proxy chains.

`sc_port` can accept a binding policy template argument:
- `SC_ONE_OR_MORE_BOUND`: The port must be bound at least once (default).
- `SC_ZERO_OR_MORE_BOUND`: The port may remain unbound (useful for optional interrupts). If accessed without binding, a fatal error is thrown.
- `SC_ALL_BOUND`: All elements of a multi-port array must be bound.

If a connection executes behavior, model it as an interface and channel. If a connection crosses structural hierarchy, expose it cleanly with an export.

## Lesson 38: LRM Bridge: Predefined Channels

Canonical lesson: https://www.learn-systemc.com/tutorials/037-lrm-bridge-predefined-channels

sc_signal, sc_buffer, resolved signals, sc_clock, sc_fifo, sc_mutex, and sc_semaphore as standard communication building blocks.

## How to Read This Lesson

This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.

The IEEE 1666 LRM provides a set of predefined primitive and hierarchical channels that form the standard toolbox for modeling hardware communication. By looking into the **Accellera SystemC kernel**, we can understand exactly how these channels manage their state and interface with the discrete-event scheduler.

## Standard and source context

## End-to-End Predefined Channels Example

Here is a complete example demonstrates how `sc_signal`, `sc_clock`, `sc_fifo`, and `sc_mutex` interoperate to model a multi-producer, clocked FIFO system.

```cpp
#include <systemc>

SC_MODULE(HardwareSystem) {
    // 1. Clock and Signals
    sc_core::sc_in<bool> clk;
    sc_core::sc_signal<bool> data_ready{"data_ready"};

    // 2. FIFO (Hierarchical Channel)
    // A thread-safe queue holding up to 4 integers
    sc_core::sc_fifo<int> data_fifo{"data_fifo", 4};

    // 3. Mutex (Primitive Channel)
    // Ensures only one producer accesses the debug port at a time
    sc_core::sc_mutex bus_mutex{"bus_mutex"};

    SC_CTOR(HardwareSystem) {
        SC_THREAD(producer_a);
        sensitive << clk.pos();

        SC_THREAD(producer_b);
        sensitive << clk.pos();

        SC_THREAD(consumer);
        sensitive << clk.pos();
    }

    void producer_a() {
        wait(2, sc_core::SC_NS); // Wait for initialization
        while(true) {
            wait(); // Wait for clock

            // Lock the mutex before printing to the shared bus/console
            bus_mutex.lock();
            std::cout << "@" << sc_core::sc_time_stamp() << " [Prod A] Acquired bus lock." << std::endl;

            // Blocking write. If FIFO is full, thread suspends here until space is available.
            data_fifo.write(0xA);
            data_ready.write(true); // Signal an update (Delta Cycle delay)

            bus_mutex.unlock();
            wait(20, sc_core::SC_NS); // Work delay
        }
    }

    void producer_b() {
        wait(5, sc_core::SC_NS);
        while(true) {
            wait();

            bus_mutex.lock();
            std::cout << "@" << sc_core::sc_time_stamp() << " [Prod B] Acquired bus lock." << std::endl;

            // Non-blocking write. If full, it fails gracefully.
            if (data_fifo.nb_write(0xB)) {
                data_ready.write(true);
            }

            bus_mutex.unlock();
            wait(15, sc_core::SC_NS);
        }
    }

    void consumer() {
        while(true) {
            wait();
            // Blocking read. If FIFO is empty, thread suspends here.
            int val = data_fifo.read();
            std::cout << "@" << sc_core::sc_time_stamp() << " [Consumer] Read value: 0x"
                      << std::hex << val << std::endl;
            data_ready.write(false);
        }
    }
};

int sc_main(int argc, char* argv[]) {
    sc_core::sc_clock clk("clk", 10, sc_core::SC_NS);
    HardwareSystem sys("sys");
    sys.clk(clk);

    sc_core::sc_start(100, sc_core::SC_NS);
    return 0;
}
```

## Channel Semantics & Kernel Implementation

### `sc_signal` and `sc_buffer`
`sc_signal<T>` applies written values during the update phase.
**Under the Hood:** In `sc_signal<T>::update()`, the kernel checks `if( !(m_new_val == m_cur_val) )`. If they differ, `m_cur_val = m_new_val` and `m_value_changed_event.notify()` is called.
`sc_buffer<T>` inherits from `sc_signal<T>` but completely overrides the `update()` method to *remove* the equality check. It assigns `m_cur_val = m_new_val` and unconditionally calls `m_value_changed_event.notify()`. Use `sc_buffer` when the *act of writing* is semantically important, even if the data didn't change.

### Resolved Signals (`sc_rv` / `sc_logic`)
SystemC provides resolved signals for multi-writer logic modeling (e.g., tri-state buses, wired-OR).
**Under the Hood:** A normal `sc_signal` throws `SC_ERROR_MULTI_WRITE` if two processes write to it in the same delta cycle. `sc_signal_resolved` intercepts multiple writes into an array. During the `update()` phase, it passes the array through a resolution table matrix defined in `sc_logic_resolution()` to determine the final electrical state (e.g., driving `Z` and `1` resolves to `1`).

### `sc_clock`
`sc_clock` provides periodic boolean-like toggling behavior.
**Under the Hood:** It is not a magical language construct. `sc_clock` is simply an `sc_module` that automatically spawns a hidden `SC_METHOD`. This method toggles the internal `sc_signal` value and uses `next_trigger(m_period / 2)` to wake up. This guarantees zero-skew edge events across the entire design.

### `sc_fifo`
`sc_fifo<T>` models queued producer-consumer communication safely across process boundaries.
**Under the Hood:** It is an `sc_prim_channel` that uses a dynamically allocated circular buffer array (`m_buf`). The blocking `write()` method contains a `while(m_num_written >= m_size)` loop that calls `wait(m_data_read_event)`. When a reader pops a value, it triggers `m_data_read_event.notify()`, waking up the suspended producer thread.

### `sc_mutex` and `sc_semaphore`
Mutexes and semaphores control shared resource access at an abstract level.
**Under the Hood:** `sc_mutex` holds an internal `sc_process_b* m_owner`. If a process calls `lock()` and `m_owner` is already set to another process, it calls `wait(m_free_event)`. When the owner calls `unlock()`, it sets `m_owner = 0` and calls `m_free_event.notify()`, allowing the scheduler to wake up the blocked process.

## Lesson 39: LRM Bridge: Datatypes

Canonical lesson: https://www.learn-systemc.com/tutorials/038-lrm-bridge-datatypes

Logic values, bit vectors, arbitrary precision integers, fixed-point types, proxies, and conversion rules in practical modeling.

## How to Read This Lesson

This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.

SystemC provides specialized hardware datatypes because standard C++ types (`int`, `bool`) cannot natively model arbitrary bus widths, 4-state logic (X/Z states), or bit-level slicing.

Let's look into the **Accellera kernel datatypes (`sc_dt` namespace)** to understand how these abstract concepts are implemented using underlying C++ constructs.

## Standard and source context

## End-to-End Datatypes Example

This fully compliant `sc_main` demonstrates the usage of 4-state logic (`sc_logic`), arbitrary width bit vectors (`sc_bv`), signed integer vectors (`sc_int`), and fixed-point math (`sc_fixed`).

```cpp
#define SC_INCLUDE_FX // Required to enable fixed-point datatypes
#include <systemc>

SC_MODULE(DatatypesDemo) {
    SC_CTOR(DatatypesDemo) {
        SC_METHOD(demo_logic_and_vectors);
        SC_METHOD(demo_fixed_point);
    }

    void demo_logic_and_vectors() {
        // 1. 4-State Logic (0, 1, Z, X)
        sc_dt::sc_logic signal_state = sc_dt::Log_Z; // High-impedance

        // 2. Bit Vectors (2-state, arbitrary width)
        sc_dt::sc_bv<12> control_bus = "101011001111";

        // 3. Logic Vectors (4-state, arbitrary width)
        sc_dt::sc_lv<8> data_bus = "10ZX0011";

        // 4. Fixed-width signed integers
        sc_dt::sc_int<9> signed_val = -25;

        // Bit-slicing proxy demonstration:
        // Extract the upper 4 bits of control_bus and assign to a new 4-bit vector
        sc_dt::sc_bv<4> upper_nibble = control_bus.range(11, 8);

        std::cout << "--- Logic & Vectors ---" << std::endl;
        std::cout << "Signal: " << signal_state << std::endl;
        std::cout << "Control Bus: " << control_bus << std::endl;
        std::cout << "Upper Nibble: " << upper_nibble << std::endl;
        std::cout << "Signed Val (9-bit): " << signed_val << std::endl;
    }

    void demo_fixed_point() {
        std::cout << "--- Fixed Point Math ---" << std::endl;

        // 16 total bits, 4 bits for the integer part (12 bits fractional)
        sc_dt::sc_fixed<16, 4> a = 3.14159;
        sc_dt::sc_fixed<16, 4> b = -1.5;

        // The result of the multiplication is truncated/rounded automatically
        // according to the sc_fixed quantization modes defined in the LRM.
        sc_dt::sc_fixed<16, 4> result = a * b;

        std::cout << "3.14159 * -1.5 in Q4.12 fixed-point = " << result.to_double() << std::endl;
    }
};

int sc_main(int argc, char* argv[]) {
    DatatypesDemo demo("demo");
    sc_core::sc_start();
    return 0;
}
```

## Type Breakdown & Kernel Implementations

### Logic and Bit Vectors
- `sc_logic`: Represents four-state logic. **Under the hood**, it is an enum (`Log_0`, `Log_1`, `Log_Z`, `Log_X`) managed within a highly optimized class that provides truth tables for `AND`, `OR`, `XOR` logic gates using array lookups.
- `sc_bv<W>`: A two-state bit vector. **Under the hood**, it allocates an array of `sc_dt::sc_digit` (which maps to a 32-bit `unsigned int` in most architectures). `sc_bv<64>` allocates two integers. Bitwise operations are translated into standard CPU integer mask operations.
- `sc_lv<W>`: A four-state logic vector. **Under the hood**, it allocates *two* parallel data arrays: one for the base data value, and one for the control mask (to represent Z and X states mathematically).

### Proxy Classes (`range` and `bit`)
When you call `control_bus.range(11, 8)`, it does **not** allocate a new vector or a string. **Under the hood**, it returns an `sc_subref` (or `sc_subref_r` for read-only). This is a **Proxy Object** that holds a pointer to the original `sc_bv` array and the bit boundaries. It heavily overloads `operator=` to execute bit-masking directly into the memory of the original array, enabling syntax like `bus.range(3,0) = 0xF;` without memory leaks.

### Fixed Width Integers
- `sc_int<W>` and `sc_uint<W>` (1 to 64 bits): Fast signed/unsigned fixed-width arithmetic. **Under the hood**, an `sc_int<12>` is stored as a full native 64-bit `int64_t`. The class overloads arithmetic operators and automatically applies bit-masks and sign-extensions to ensure the result exactly matches the behavior of a physical 12-bit ALU.
- `sc_bigint<W>` and `sc_biguint<W>`: Arbitrary precision arithmetic. Stored as an array of `sc_digit`s. Multiplication involves software `O(N^2)` long-multiplication algorithms.

### Fixed-Point (`sc_fixed`)
To use fixed-point types, you must define `#define SC_INCLUDE_FX` before including `<systemc>`. `sc_fixed<WL, IWL>` models word length (`WL`) and integer word length (`IWL`). The LRM provides exhaustive quantization and overflow modes (e.g., `SC_RND`, `SC_TRN`, `SC_SAT`) for DSP and AMS modeling, implemented using complex macro expansions and bit-shifts in `sc_fxnum`.

## Practical Modeling Discipline

While bit selection proxies are syntactically convenient, dense bit-slicing equations are notoriously difficult to debug and slow down simulation due to proxy object construction. For Virtual Platforms and TLM, prefer standard C++ types (`uint32_t`, `uint64_t`) for data payloads and registers to maximize simulation speed, resorting to `sc_bv` only when bit-accurate pin modeling is explicitly required.

## Lesson 40: LRM Bridge: Reports, Tracing, and Introspection

Canonical lesson: https://www.learn-systemc.com/tutorials/039-lrm-bridge-reports-tracing-and-introspection

Reporting severity, report handlers, VCD tracing, object hierarchy, attributes, version information, and diagnostics.

## How to Read This Lesson

This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.

Diagnostics are a formalized part of the SystemC standard surface. Standardizing error reporting and trace generation ensures interoperability between different IP vendors and testbench infrastructures. Now let's look at how the **Accellera kernel** implements these facilities natively.

## Standard and source context

## End-to-End Tracing and Reporting Example

Here is a complete example demonstrates the `SC_REPORT` macros, custom message types, and generating a standard Value Change Dump (VCD) trace file.

```cpp
#include <systemc>

SC_MODULE(DiagnosticsDemo) {
    sc_core::sc_in<bool> clk;
    sc_core::sc_signal<uint32_t> counter{"counter"};

    SC_CTOR(DiagnosticsDemo) {
        SC_THREAD(logic_thread);
        sensitive << clk.pos();
    }

    void logic_thread() {
        uint32_t val = 0;
        while(true) {
            wait();
            val++;
            counter.write(val);

            // Standard reporting
            SC_REPORT_INFO("DiagnosticsDemo", "Counter incremented.");

            if (val == 3) {
                // Warning with a custom message type ID
                SC_REPORT_WARNING("LIMIT_CHECK", "Counter reached 3. Approaching limit.");
            }
            if (val == 5) {
                // Error report. By default, SC_REPORT_ERROR will stop the simulation.
                SC_REPORT_ERROR("LIMIT_CHECK", "Counter overflowed the simulated limit!");
            }
        }
    }
};

int sc_main(int argc, char* argv[]) {
    sc_core::sc_clock clk("clk", 10, sc_core::sc_time_unit::SC_NS);

    DiagnosticsDemo demo("demo");
    demo.clk(clk);

    // 1. Create a VCD trace file
    sc_core::sc_trace_file* tf = sc_core::sc_create_vcd_trace_file("wave_trace");

    // 2. Trace signals
    sc_core::sc_trace(tf, clk, "System_Clock");
    sc_core::sc_trace(tf, demo.counter, "Counter_Value");

    std::cout << "Starting simulation. This will terminate automatically on SC_REPORT_ERROR." << std::endl;

    // 3. Start Simulation
    sc_core::sc_start(100, sc_core::SC_NS);

    // 4. Close the trace file gracefully
    sc_core::sc_close_vcd_trace_file(tf);

    return 0;
}
```

## Reports and Handling Policies

Never use `std::cout` or `printf` for structural warnings or errors, and **never call `std::exit()`** from inside a reusable IP block.

Use the standard macros:
- `SC_REPORT_INFO(msg_type, msg)`
- `SC_REPORT_WARNING(msg_type, msg)`
- `SC_REPORT_ERROR(msg_type, msg)`: By default, throws an exception and halts the scheduler.
- `SC_REPORT_FATAL(msg_type, msg)`: Immediately aborts execution entirely.

**Under the Hood (Accellera Kernel):**
When you use a macro like `SC_REPORT_WARNING`, it generates a `sc_report` object containing the file, line number, time, and message. This object is passed to a global singleton `sc_report_handler::report()`. The handler looks up the `sc_actions` bitmask configured for that specific `msg_type` and severity. If the action bitmask includes `SC_THROW`, it literally throws an `sc_report` exception. If it contains `SC_ABORT`, it calls `std::abort()`.

### Report Handlers
The simulation environment (often the `sc_main` testbench) can override these default actions. An application can configure all warnings of `msg_type` `"LIMIT_CHECK"` to be suppressed using `sc_report_handler::set_actions("LIMIT_CHECK", SC_WARNING, SC_DO_NOTHING)`, or demote an `SC_REPORT_ERROR` to merely increment an internal counter rather than halting the simulation.

## Tracing

Tracing records signal activity into a file.
```cpp
auto* tf = sc_create_vcd_trace_file("wave");
sc_trace(tf, signal, "signal_name");
```
Trace files must be created before `sc_start()` is called, and gracefully closed (`sc_close_vcd_trace_file`) at the end of `sc_main()`.

**Under the Hood:**
`sc_create_vcd_trace_file()` instantiates a `vcd_trace_file` object and pushes it into the `sc_simcontext::m_trace_files` vector.
During simulation, how do values get dumped? Inside the `sc_simcontext::crunch()` loop, after the Evaluate and Update phases have finished for the current delta cycle, the kernel iterates over `m_trace_files` and calls their internal `cycle(true)` virtual method. This method iterates over all traced signals. If the value has changed since the last delta, it writes the formatted string mapping (e.g., `b1011 $n`) to the file stream.

## Object Hierarchy

SystemC objects know their names and hierarchy. Hierarchy is how a large model remains navigable and debuggable. You can always query an object's location in the elaboration tree via `this->name()` (e.g., `top.router_0.timer_1`).

**Under the Hood:**
Every `sc_module` and `sc_signal` derives from `sc_object`. During construction, they register themselves with `sc_simcontext::m_object_manager`. This manager links them into a massive tree structure mapping parent objects to their children, enabling tools to introspect the entire topology dynamically.

## Source-reading checkpoint

For reports and tracing, start in the Accellera SystemC GitHub repository around report-handler and trace-file code, then follow the `sc_object` names surfaced by introspection.

## Lesson 41: LRM Bridge: TLM Reference Map

Canonical lesson: https://www.learn-systemc.com/tutorials/040-lrm-bridge-tlm-reference-map

Blocking transport, non-blocking transport, generic payload, phases, sockets, DMI, debug transport, and temporal decoupling.

## How to Read This Lesson

This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.

Transaction Level Modeling (TLM 2.0), defined in the IEEE 1666 standard, allows models to communicate through abstract C++ data structures (`tlm_generic_payload`) rather than individual pin toggles. This can dramatically increase simulation speed when the abstraction is chosen well.

Now let's look at how the **Accellera TLM kernel** defines these constructs under the hood.

## Standard and source context

## Complete TLM 2.0 Base Protocol Example

This `sc_main` example demonstrates the essential components of TLM: the generic payload, socket binding, blocking transport, and correct target response mechanics.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>

// 1. TLM Initiator
SC_MODULE(TLM_Initiator) {
    tlm_utils::simple_initiator_socket<TLM_Initiator> socket;

    SC_CTOR(TLM_Initiator) : socket("socket") {
        SC_THREAD(run);
    }

    void run() {
        tlm::tlm_generic_payload trans;
        sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
        uint32_t data = 0x12345678;

        // Populate the Generic Payload (Strict LRM rules)
        trans.set_command(tlm::TLM_WRITE_COMMAND);
        trans.set_address(0x1000);
        trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
        trans.set_data_length(4);
        trans.set_streaming_width(4);
        trans.set_byte_enable_ptr(0); // 0 means all bytes enabled
        trans.set_dmi_allowed(false);
        trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

        std::cout << "@" << sc_core::sc_time_stamp() << " [Init] Sending WRITE to 0x1000" << std::endl;

        // Blocking Transport Call
        socket->b_transport(trans, delay);

        // Check Response
        if (trans.is_response_error()) {
            SC_REPORT_ERROR("TLM", "Transaction returned with error response!");
        } else {
            std::cout << "@" << sc_core::sc_time_stamp() << " [Init] WRITE Success. Delay returned: "
                      << delay << std::endl;
            wait(delay); // Yield to the scheduler to account for annotated delay
        }
    }
};

// 2. TLM Target
SC_MODULE(TLM_Target) {
    tlm_utils::simple_target_socket<TLM_Target> socket;

    SC_CTOR(TLM_Target) : socket("socket") {
        socket.register_b_transport(this, &TLM_Target::b_transport);
    }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        if (trans.get_address() == 0x1000 && trans.get_command() == tlm::TLM_WRITE_COMMAND) {
            // Annotate processing delay
            delay += sc_core::sc_time(10, sc_core::SC_NS);

            // MANDATORY: Target must explicitly set the response status
            trans.set_response_status(tlm::TLM_OK_RESPONSE);
        } else {
            trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
        }
    }
};

int sc_main(int argc, char* argv[]) {
    TLM_Initiator init("init");
    TLM_Target target("target");

    init.socket.bind(target.socket);

    sc_core::sc_start();
    return 0;
}
```

## Transport Interfaces & Implementations

### Blocking Transport (`b_transport`)
Used for Loosely Timed (LT) modeling. **Under the Hood:** `b_transport` is simply a purely virtual C++ method declared in `tlm_fw_transport_if`. When an initiator calls it, because of `sc_port` proxy resolution, the execution jumps directly into the target's function implementation within the very same OS thread context. No event queues are involved.

### Non-Blocking Transport (`nb_transport_fw` / `nb_transport_bw`)
Used for Approximately Timed (AT) models. Splits a transaction into explicit hardware phases (e.g., `BEGIN_REQ`, `END_REQ`, `BEGIN_RESP`, `END_RESP`) using the `tlm_phase` enumeration class. This supports deep pipelining and complex interconnect arbitration but is significantly slower to simulate because each phase boundary often requires a `sc_event::notify()` and a context switch.

## Advanced Interfaces

### Direct Memory Interface (DMI)
DMI allows an initiator to request a direct C++ pointer to a target's memory array.
**Under the Hood:** An initiator calls `get_direct_mem_ptr` which populates a `tlm_dmi` struct. This struct holds `unsigned char* dmi_ptr`, bounding addresses `dmi_start_address`/`dmi_end_address`, and `dmi_read_allowed`/`dmi_write_allowed` enums. If granted, the initiator bypasses the `b_transport` socket entirely, performing native C++ array dereferencing (`dmi_ptr[offset]`). The LRM requires targets to use `invalidate_direct_mem_ptr` to revoke this pointer if the memory map changes.

### Debug Transport (`transport_dbg`)
Debug transport is a side-effect-free access path. It is used exclusively by debuggers, GDB stubs, or memory preloaders. A debug read to a FIFO must **not** pop the FIFO (you must peek instead), and it does not advance simulation time.

### Temporal Decoupling
Temporal decoupling allows initiators to run ahead of global simulation time locally within a "quantum".
**Under the Hood:** The Accellera TLM library provides the `tlm_quantumkeeper` class. Initiators accumulate delays internally (`m_local_time`) without calling `wait()`. The keeper continuously calculates the difference between `m_local_time` and `sc_core::sc_time_stamp()`. Only when this difference exceeds a globally defined `tlm_global_quantum` does the keeper call `wait()` to sync back up with the discrete-event scheduler. This drastically reduces context switches, offering the highest possible simulation speed.

## Source-reading checkpoint

For the TLM reference map, inspect `tlm_core` beside the `sc_port` and `sc_export` lessons. Socket convenience types still rely on the core binding machinery underneath.

## Lesson 42: LRM Standards Version Map

Canonical lesson: https://www.learn-systemc.com/tutorials/041-lrm-standards-version-map

Understanding the chronological evolution of IEEE 1666, SystemC AMS, CCI, and UVM-SystemC standards.

## How to Read This Lesson

This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.

# LRM Standards Version Map

The SystemC ecosystem is defined by several inter-locking standards managed by Accellera and IEEE. Understanding the version history is critical for ensuring compliance and compiler compatibility. Furthermore, examining the **Accellera GitHub repositories** reveals how these versions are enforced at compile time.

## Standard and source context

## 1. SystemC Core (IEEE 1666)

- **IEEE 1666-2005:** The first formal IEEE standardization of SystemC. Introduced core discrete-event simulation, SC_METHOD, SC_THREAD, events, and standard data types.
- **IEEE 1666-2011:** The landmark update that officially merged **TLM 2.0** into the core language standard. Introduced process control (`sc_process_handle`), `sc_spawn`, and async resets.
- **IEEE 1666-2023 (Current):** The modern standard. **Under the Hood:** The official Accellera `systemc` GitHub repository updated its `CMakeLists.txt` to strictly mandate `CMAKE_CXX_STANDARD 14` (compatible up to C++17/C++20). This update replaced many internal macro-hacks with native C++11/14 features (like `std::unique_ptr` in internal object managers and `override` specifiers), introduced stages of elaboration callbacks (`before_end_of_elaboration`, etc.), and formalized memory management semantics.

## 2. SystemC AMS (Analog/Mixed-Signal)

- **AMS 1.0 (2010):** Introduced Timed Data Flow (TDF), Linear Signal Flow (LSF), and Electrical Linear Networks (ELN).
- **AMS 2.0 (2016 - Current):** Defined dynamic TDF rates, improved continuous-time solver synchronization with the IEEE 1666 discrete kernel, and added standard AMS tracing capabilities.

## 3. SystemC CCI (Configuration, Control, and Inspection)

- **CCI 1.0 (2018):** Standardized the `cci_param` and broker APIs for configuring Virtual Platforms without proprietary string parsing. **Under the Hood:** The `cci` repo integrates cleanly with `sc_object`, leveraging the 1666 hierarchical string mapping to provide JSON-like `cci_value` configuration trees.

## 4. UVM-SystemC

- **UVM-SystemC public-review drafts:** UVM-SystemC brings the UVM methodology into SystemC/C++. Accellera published a `1.0-beta6` public-review release signal in July 2024. The local February 2023 draft LRM is useful for clause reading, while the official `uvm-systemc` repository is the implementation trail to inspect for release-specific behavior. **Under the Hood:** the implementation uses SystemC process facilities and C++ type mechanisms to support phases, factory behavior, and reusable verification components.

## Basic Compliant Starter

A compliant model today explicitly leverages the `sc_core` namespace and standard macros.

**Under the Hood:** `sc_core::sc_version()` returns a statically compiled string generated during the library's CMake build process (usually combining `SC_VERSION`, `SC_VERSION_RELEASE_DATE`, and `SC_VERSION_PRERELEASE` macros from `sysc/kernel/sc_ver.h`), ensuring you can programmatically verify the kernel version.

```cpp
#include <systemc>

// A fully IEEE 1666-2023 compliant stub
SC_MODULE(CompliantModel) {
    SC_CTOR(CompliantModel) {
        SC_REPORT_INFO("Version", sc_core::sc_version());
    }
};

int sc_main(int argc, char* argv[]) {
    CompliantModel model("model");
    sc_core::sc_start();
    return 0;
}
```

## Lesson 43: LRM Expert Reading Method

Canonical lesson: https://www.learn-systemc.com/tutorials/042-lrm-expert-reading-method

How to read and interpret the IEEE 1666 LRM document effectively for deep technical modeling.

## How to Read This Lesson

This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.

# LRM Expert Reading Method

The IEEE 1666 Language Reference Manual (LRM) is an intimidating PDF of over 600 pages. It is written in strict legalistic language, defining precisely what is allowed (Normative) versus what is just explanatory (Informative).

To become an expert SystemC architect, you must know how to parse this document and map it directly to the **Accellera C++ source code**.

## Standard and source context

## 1. "Shall" vs "Should" and `SC_REPORT_FATAL`

In IEEE standards, words have absolute technical meanings:
- **SHALL:** A strict requirement. If your code violates a "shall," your model is illegal and its behavior is undefined.
  - **Under the Hood:** In the Accellera kernel, many "shall" clauses are explicitly mapped to runtime checks using `sc_assert()` or `SC_REPORT_FATAL()`. For example, the rule "Time resolution shall only be set before simulation starts" is enforced in `sc_simcontext::set_time_resolution` which checks `if(m_simulation_status != SC_ELABORATION) SC_REPORT_ERROR(SC_ID_SET_TIME_RESOLUTION_, "");`.
- **SHOULD:** A strong recommendation. You can ignore it, but you better have an exceptional architectural reason.
- **MAY:** An optional feature.

However, for performance reasons, not every "shall" is protected by an assertion! For example, many TLM memory management "shalls" have no runtime assertions. If you violate them, the C++ code simply segfaults.

## 2. Navigating the Clauses and Source Files

Do not read the LRM front-to-back. Treat it as a technical dictionary mapping to the Accellera GitHub repository:
- **Clause 4 (Elaboration and Simulation):** Maps directly to `src/sysc/kernel/sc_simcontext.cpp`. Read this to understand why your `sc_start()` is hanging. It mathematically defines the Delta Cycle (`crunch()` loop).
- **Clause 5 (Core Language):** Maps to `src/sysc/kernel/sc_module.cpp` and `sc_process.cpp`. Read this to understand exactly what `sc_module` and `SC_THREAD` are doing under the hood.
- **Clause 11-16 (TLM 2.0):** Maps to `src/tlm_core/tlm_2/`. Read this to understand the precise rules of generic payloads, memory managers, and socket binding.

## Practical Example: Reading the TLM Rules

If you read Clause 14 on the `tlm_generic_payload`, you will see a rule:
*"The target **shall** set the response status attribute to a value other than `TLM_INCOMPLETE_RESPONSE` before passing the transaction object back to the initiator."*

This is why, in all our TLM examples, you see this exact code pattern:

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>

SC_MODULE(CompliantTarget) {
    tlm_utils::simple_target_socket<CompliantTarget> socket;

    SC_CTOR(CompliantTarget) : socket("socket") {
        socket.register_b_transport(this, &CompliantTarget::b_transport);
    }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        // ... process data ...

        // Fulfilling the "shall" requirement from the LRM
        // Without this, the initiator's check trans.is_response_error() might behave unpredictably.
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
    }
};

int sc_main(int argc, char* argv[]) {
    // Boilerplate main
    return 0;
}
```

By reading the LRM closely and cross-referencing with the `accellera-official/systemc` repository, you stop guessing why things crash and start architecting deterministic hardware models.

## Lesson 44: SystemC AMS (Analog/Mixed-Signal)

Canonical lesson: https://www.learn-systemc.com/tutorials/043-systemc-ams-analog-mixed-signal

An introduction to the SystemC AMS standard, Timed Data Flow (TDF), and modeling continuous-time signals.

## How to Read This Lesson

# SystemC AMS (Analog/Mixed-Signal)

Standard SystemC is a **discrete-event** simulator. It evaluates state changes at specific, discrete points in time.

However, modern SoCs interact heavily with continuous-time physical systems (RF transceivers, sensors, power grids). To model these systems efficiently without resorting to cycle-heavy, numerically intense SPICE-level simulations, Accellera standardized the **SystemC AMS 2.0 Language Reference Manual**.

## Standard and source context

## The Three Models of Computation in AMS

SystemC AMS introduces three mathematically distinct Models of Computation (MoC) that interoperate with the discrete SystemC kernel:

1. **Timed Data Flow (TDF):** The most common MoC. It uses a static schedule and fixed time steps. Best for signal processing, communication algorithms, and behavioral analog modeling.
2. **Linear Signal Flow (LSF):** Used to model continuous-time behavior using primitive blocks like integrators, differentiators, and adders.
3. **Electrical Linear Networks (ELN):** Models electrical circuits using passive components (resistors, capacitors, inductors) and sources. Solved via nodal analysis matrices.

This guide focuses on **TDF**, as it bridges the gap between digital discrete control and continuous analog waveforms efficiently.

## Writing a TDF Module & Accellera Kernel Details

In AMS TDF, you inherit from `sca_tdf::sca_module` and define a fixed-time calculation in the `processing()` callback.

Unlike discrete SystemC, TDF modules do not wait on events (no `sc_event`). They are statically scheduled by the AMS solver to execute at exact, regular intervals dictated by `set_timestep()`.

**Under the Hood (Accellera `systemc-ams` repository):**
`sca_tdf::sca_module` registers itself with an internal synchronization layer (`sca_core::sca_implementation::sca_sync_obj` and `sca_solver_base`). During elaboration, the AMS kernel identifies all connected TDF modules and forms a "cluster." It performs a static scheduling algorithm to determine the exact order in which `processing()` must be called to satisfy data dependencies without deadlocks.
When `sc_start()` is called, the AMS solver actually registers a standard `SC_METHOD` within the standard SystemC `sc_simcontext`. This hidden method computes the next time the TDF cluster needs to run and issues a `sc_core::next_trigger(sc_time)` to sleep the continuous-time solver until the discrete-event time matches the next TDF time step.

### Complete AMS Interoperability Example

Here is a complete `sc_main` example models an Analog-to-Digital Converter (ADC). It reads an analog sine wave generated in TDF, converts it to a discrete digital signal, and triggers a standard SystemC thread when the voltage crosses a threshold.

```cpp
#include <systemc>
#include <systemc-ams.h>

// 1. TDF Sine Wave Generator (Purely Analog)
SCA_TDF_MODULE(SineGenerator) {
    sca_tdf::sca_out<double> analog_out;

    double amplitude;
    double frequency;

    SCA_CTOR(SineGenerator) : amplitude(3.3), frequency(1000.0) {}

    // Defines the strict execution interval for the solver
    void set_attributes() {
        set_timestep(10, sc_core::SC_US);
    }

    void processing() {
        double current_time = get_time().to_seconds();
        double value = amplitude * std::sin(2.0 * M_PI * frequency * current_time);
        analog_out.write(value);
    }
};

// 2. TDF ADC (Mixed-Signal: Analog In, Digital Out)
SCA_TDF_MODULE(ADC_Model) {
    sca_tdf::sca_in<double> analog_in;
    // sca_de denotes Discrete-Event (standard SystemC) output
    sca_tdf::sca_de::sca_out<bool> threshold_alert;

    SCA_CTOR(ADC_Model) {}

    void set_attributes() {
        set_timestep(10, sc_core::SC_US);
    }

    void processing() {
        double voltage = analog_in.read();

        // If voltage exceeds 3.0V, trigger the digital alert
        if (voltage > 3.0) {
            threshold_alert.write(true); // Schedules a SystemC delta update!
        } else {
            threshold_alert.write(false);
        }
    }
};

// 3. Standard SystemC Digital Monitor
SC_MODULE(DigitalMonitor) {
    sc_core::sc_in<bool> alert_in;

    SC_CTOR(DigitalMonitor) {
        SC_THREAD(monitor_thread);
        sensitive << alert_in.pos(); // Wake up exactly when the AMS model crosses 3.0V
    }

    void monitor_thread() {
        while(true) {
            wait();
            std::cout << "@" << sc_core::sc_time_stamp()
                      << " [Digital Domain] WARNING: Voltage crossed 3.0V threshold!" << std::endl;
        }
    }
};

int sc_main(int argc, char* argv[]) {
    // Standard SystemC signal bridging the two domains
    sc_core::sc_signal<bool> alert_sig("alert_sig");
    // AMS continuous-time signal (using sca_tdf::sca_signal)
    sca_tdf::sca_signal<double> analog_wire("analog_wire");

    // Instantiate modules
    SineGenerator sine("sine");
    sine.analog_out(analog_wire);

    ADC_Model adc("adc");
    adc.analog_in(analog_wire);
    adc.threshold_alert(alert_sig);

    DigitalMonitor monitor("monitor");
    monitor.alert_in(alert_sig);

    std::cout << "Starting Mixed-Signal Simulation..." << std::endl;
    // Simulate for 2 milliseconds
    sc_core::sc_start(2, sc_core::SC_MS);

    return 0;
}
```

## Bridging the Gap: Converter Ports

The magic in the example above lies in `sca_tdf::sca_de::sca_out<bool>`.
To cross domains, SystemC AMS provides specialized converter ports:

- **`sca_tdf::sca_de::sca_in<T>`**: Reads a standard SystemC `sc_signal<T>` from within the TDF `processing()` method.
- **`sca_tdf::sca_de::sca_out<T>`**: Writes to a standard SystemC `sc_signal<T>`.
  - **Under the Hood:** When `sca_tdf::sca_de::sca_out::write(val)` is called inside the `processing()` callback, the AMS solver delegates the call to the bound `sc_signal<T>::write(val)`. Because this standard SystemC `write()` operates normally, it triggers an `request_update()` in the SystemC discrete-event kernel. In the next discrete-event Update Phase, standard SystemC processes sensitive to that signal are made runnable.

## Summary

SystemC AMS allows you to model RF, power, and analog subsystems within the same executable as your digital RTL and TLM firmware. By leveraging TDF and understanding how the AMS cluster scheduling integrates natively with the `sc_simcontext` time queue, you achieve extremely fast simulation speeds while maintaining enough accuracy for architectural exploration, power modeling, and mixed-signal verification.

## Lesson 45: Introduction to SystemC AMS and Models of Computation

Canonical lesson: https://www.learn-systemc.com/tutorials/044-introduction-to-systemc-ams-and-models-of-computat

An overview of the SystemC Analog/Mixed-Signal (AMS) extensions and its three primary Models of Computation (MoCs).

## How to Read This Lesson

# Introduction to SystemC AMS and Models of Computation

Welcome to the SystemC Analog/Mixed-Signal (AMS) extensions. Standard SystemC models hardware using a Discrete-Event (DE) simulation kernel, which is excellent for digital logic. However, modern systemsâ€”such as RF communication interfaces, automotive engine controllers, and power management ICsâ€”tightly interweave digital control logic with continuous-time analog behavior.

The **SystemC AMS extensions (IEEE 1666.1)** bridge this gap by enabling the co-simulation of discrete-event digital behavior alongside continuous-time and discrete-time analog equations.

## Standard and source context

## Why SystemC AMS?

Traditional SPICE simulators calculate voltages and currents at the transistor level, which is far too slow for system-level verification where a digital firmware boot might take millions of clock cycles. SystemC AMS abstracts the analog behavior into higher-level mathematical models, allowing execution speeds that are orders of magnitude faster than SPICE, while maintaining accuracy appropriate for architectural exploration and software bring-up.

## The Three Models of Computation (MoCs)

To handle the diverse requirements of analog modeling, the SystemC AMS standard defines three distinct **Models of Computation (MoCs)**.

### 1. Timed Data Flow (TDF)
The **Timed Data Flow (TDF)** MoC is the workhorse of SystemC AMS. It models systems using discrete-time sampled signals. TDF is incredibly fast because it relies on static scheduling. Before the simulation starts, the TDF solver analyzes the data rates, timesteps, and delays of all connected TDF modules and creates a fixed mathematical execution schedule.
* **Use Case:** Digital Signal Processing (DSP), baseband communications, and abstract control loops.

### 2. Linear Signal Flow (LSF)
The **Linear Signal Flow (LSF)** MoC defines behavior through continuous-time mathematical relations (differential and algebraic equations). You build an LSF model by connecting predefined primitive blocks like adders, integrators, and differentiators. The AMS linear solver evaluates these equations dynamically.
* **Use Case:** Analog control systems, Laplace-domain transfer functions, and abstract filters.

### 3. Electrical Linear Networks (ELN)
The **Electrical Linear Networks (ELN)** MoC provides a traditional node-based electrical modeling approach. It uses conservative continuous-time equations based on Kirchhoff's Voltage and Current Laws. You connect electrical primitives like resistors, capacitors, inductors, and voltage/current sources via terminals.
* **Use Case:** Modeling electrical loads, power drivers, and basic RLC circuits.

## Clusters and Solvers

SystemC AMS introduces the concept of a **cluster**. When you instantiate and connect AMS modules, the AMS kernel traverses the netlist during elaboration and groups connected modules of the same MoC into clusters. Each cluster is assigned its own mathematical solver (e.g., a Linear Solver for ELN/LSF, or a Static Scheduler for TDF).

These solvers run synchronously with the standard SystemC DE kernel, ensuring that time progresses accurately across both the digital and analog domains.

## Under the Hood: Clusters and the Synchronization Thread

In the C++ Accellera SystemC AMS reference implementation, the boundary between the AMS world and the standard SystemC Discrete Event (DE) kernel is carefully managed.

During the `end_of_elaboration` phase, the AMS kernel traverses all `sca_module` instances and groups them into contiguous graphs called **clusters**. For a TDF cluster, the kernel builds a Directed Acyclic Graph (DAG) based on the `sca_in` and `sca_out` port connections. It calculates the Greatest Common Divisor (GCD) of all module timesteps to determine the **Cluster Period**.

Once the static schedule is computed, the AMS kernel does not run its own isolated simulation loop. Instead, for each cluster, it dynamically spawns an `sc_core::SC_THREAD` inside the standard SystemC kernel. This thread executes the static schedule (calling your `processing()` methods in the computed order). At the end of the cluster period, the thread calls `sc_core::wait(cluster_period)`, suspending itself and explicitly yielding control back to the SystemC DE scheduler. This architectural design ensures that AMS math solvers and digital SystemC processes interleave perfectly without race conditions or causality violations.

## Complete Example: Your First AMS Simulation

The following complete, compilable example demonstrates how to set up a basic SystemC AMS simulation. It uses the TDF MoC to generate a sine wave, scales it by a constant, and traces the output to a file that can be viewed in a waveform viewer (like GTKWave).

```cpp
#include <systemc>
#include <systemc-ams.h>
#include <cmath>

// 1. A TDF Source Module generating a Sine Wave
SCA_TDF_MODULE(SineSource) {
    sca_tdf::sca_out<double> out;

    double amplitude;
    double frequency;

    SCA_CTOR(SineSource) : amplitude(1.0), frequency(1000.0) {}

    // set_attributes is called by the solver during elaboration
    void set_attributes() {
        // Define the sampling period (timestep)
        set_timestep(10.0, sc_core::SC_US);
    }

    // processing is called at every timestep according to the static schedule
    void processing() {
        double t = get_time().to_seconds(); // Get current simulation time
        double val = amplitude * std::sin(2.0 * M_PI * frequency * t);
        out.write(val);
    }
};

// 2. A TDF Sink Module acting as an amplifier
SCA_TDF_MODULE(Amplifier) {
    sca_tdf::sca_in<double> in;
    sca_tdf::sca_out<double> out;

    double gain;

    SCA_CTOR(Amplifier) : gain(2.5) {}

    void set_attributes() {
        // TDF automatically propagates timesteps.
        // We do not strictly need to set the timestep here because it will
        // inherit the 10us timestep from the SineSource through the connected signal.
    }

    void processing() {
        // Read the input, apply gain, write to output
        out.write(in.read() * gain);
    }
};

int sc_main(int argc, char* argv[]) {
    // 3. Declare AMS Signals to connect the modules
    sca_tdf::sca_signal<double> sig_sine("sig_sine");
    sca_tdf::sca_signal<double> sig_amp("sig_amp");

    // 4. Instantiate modules
    SineSource src("src");
    src.out(sig_sine);

    Amplifier amp("amp");
    amp.in(sig_sine);
    amp.out(sig_amp);

    // 5. Setup Tracing
    // SystemC AMS provides specialized tracing functions for analog waveforms.
    // This creates a standard VCD (Value Change Dump) file.
    sca_util::sca_trace_file* tf = sca_util::sca_create_vcd_trace_file("ams_waveforms");

    // Trace the signals
    sca_util::sca_trace(tf, sig_sine, "SineWave");
    sca_util::sca_trace(tf, sig_amp, "AmplifiedWave");

    // 6. Start the simulation
    std::cout << "Starting AMS Simulation...\n";
    sc_core::sc_start(2.0, sc_core::SC_MS); // Simulate for 2 milliseconds
    std::cout << "Simulation Complete.\n";

    // 7. Cleanup
    sca_util::sca_close_vcd_trace_file(tf);

    return 0;
}
```

In the next tutorial, we will dive deeper into the rules and advanced callbacks of the **Timed Data Flow (TDF)** model.

## Lesson 46: Timed Data Flow (TDF)

Canonical lesson: https://www.learn-systemc.com/tutorials/045-timed-data-flow-tdf

A deep dive into the Timed Data Flow (TDF) model of computation, including ports, modules, attributes, and processing phases.

## How to Read This Lesson

# Timed Data Flow (TDF)

The **Timed Data Flow (TDF)** model of computation is the most commonly used MoC in SystemC AMS. It is designed to model signal processing algorithms, feedback loops, and communication systems using discrete-time sampling.

TDF is highly efficient because the activation schedule of a set of connected TDF modules is computed **statically** before the simulation starts. The solver analyzes the number of samples read and written by each module (the rates) and builds a fixed, deterministic schedule that minimizes overhead.

## Standard and source context

## The Structure of a TDF Module

According to the IEEE 1666.1 standard, a TDF module must derive from `sca_tdf::sca_module` (or use the handy `SCA_TDF_MODULE` macro). Unlike standard SystemC modules, TDF modules do not use `SC_METHOD` or `SC_THREAD`. Instead, they define their behavior using specific virtual callbacks:

1. **`set_attributes()`**: Called during the elaboration phase. This is where you configure the properties of the module and its ports (like time steps, rates, and delays).
2. **`initialize()`**: Called exactly once at the beginning of the simulation. Used to set initial conditions or to write initial delay samples into output ports.
3. **`processing()`**: The core time-domain behavior of the module. This is called repeatedly during simulation according to the statically computed schedule.
4. **`change_attributes()`** (Dynamic TDF): Evaluated during simulation to allow dynamic adaptation of timesteps or rates (introduced in AMS 2.0).

## Core TDF Attributes: Timestep, Rate, and Delay

To build the static schedule, the TDF solver relies on three fundamental attributes assigned to ports and modules during `set_attributes()`:

1. **Timestep (`set_timestep`)**: The time interval between two consecutive samples. If assigned to a module, it defines the period at which the `processing()` function is activated.
2. **Rate (`set_rate`)**: The number of samples read or written to a port per module activation. By default, the rate is 1. If a port has a rate of $R$, the module must read/write exactly $R$ samples every time `processing()` is called.
3. **Delay (`set_delay`)**: The number of samples inserted at a port before the first actual output sample is produced. This acts as a mathematical register (`z^-1`) and is absolutely required to break algebraic loops in feedback circuits.

### The Consistency Equation

For the TDF solver to function, the assigned timesteps and rates must be mathematically consistent across the entire cluster. The relationship between a module's timestep ($T_m$), a port's timestep ($T_p$), and the port's rate ($R$) is strictly defined as:

$$T_m = T_p \times R$$

## Complete Example: Multirate TDF Mixer

Here is a complete, compilable example demonstrates a classic communication system block: a Mixer. It takes an RF input and a Local Oscillator (LO) input, multiplies them to produce an Intermediate Frequency (IF), and then uses a **Multirate** Decimator to downsample the output.

```cpp
#include <systemc>
#include <systemc-ams.h>

// 1. A Simple TDF Source
SCA_TDF_MODULE(Oscillator) {
    sca_tdf::sca_out<double> out;
    double frequency;

    SCA_CTOR(Oscillator) : frequency(1e6) {} // 1 MHz default

    void set_attributes() {
        set_timestep(0.1, sc_core::SC_US); // 10 MHz sampling rate
    }

    void processing() {
        double t = get_time().to_seconds();
        out.write(std::sin(2.0 * M_PI * frequency * t));
    }
};

// 2. The Mixer
SCA_TDF_MODULE(Mixer) {
    sca_tdf::sca_in<double> rf_in;
    sca_tdf::sca_in<double> lo_in;
    sca_tdf::sca_out<double> if_out;

    SCA_CTOR(Mixer) {}

    void set_attributes() {
        // We do not need to set the timestep here.
        // The solver will automatically inherit the 0.1us timestep
        // from the oscillators connected to our inputs.
    }

    void processing() {
        // Rates default to 1. We read exactly one sample from each input,
        // and write exactly one sample to the output.
        double rf_val = rf_in.read();
        double lo_val = lo_in.read();
        if_out.write(rf_val * lo_val);
    }
};

// 3. A Multirate Decimator (Downsampler)
SCA_TDF_MODULE(Decimator) {
    sca_tdf::sca_in<double> in;
    sca_tdf::sca_out<double> out;

    SCA_CTOR(Decimator) {}

    void set_attributes() {
        // We will read 4 samples for every 1 sample we write out.
        // This makes our output port timestep 4x slower than our input port timestep.
        in.set_rate(4);
        out.set_rate(1);

        // We add a delay of 1 sample to the output to demonstrate breaking algebraic loops
        out.set_delay(1);
    }

    void initialize() {
        // Because we set a delay of 1 on 'out', we MUST write 1 initial sample
        // during initialize() to prime the delay buffer.
        out.initialize(0.0);
    }

    void processing() {
        double sum = 0;
        // We MUST read exactly 'in.get_rate()' samples (4 samples)
        for (unsigned int i = 0; i < in.get_rate(); ++i) {
            sum += in.read(i); // Read at specific index
        }

        // We write exactly 'out.get_rate()' samples (1 sample)
        double average = sum / 4.0;
        out.write(average);
    }
};

## Under the Hood: TDF Ports and Ring Buffers

To understand why TDF achieves such high execution speeds, look at the implementation of `sca_tdf::sca_in` and `sca_tdf::sca_out`. Unlike standard SystemC `sc_in`, which triggers an `sc_event` and requires a full context switch through the DE kernel on every single read/write, TDF ports are completely decoupled from SystemC events during the continuous `processing()` phase.

When the AMS solver finishes the `end_of_elaboration` phase, it calculates the maximum required size for each connected `sca_tdf::sca_signal` based on the sum of the rates and delays of the attached ports. The kernel then pre-allocates an internal C++ `std::vector` (or a flat array acting as a cyclic ring buffer) for each signal.

During the `processing()` loop, calling `in.read(i)` or `out.write(val, i)` does not interact with the SystemC kernel at all. It compiles down to a raw, inlined C++ array pointer dereference (`buffer[(read_pointer + i) % size]`). This architecture ensures absolutely zero heap allocations and zero SystemC event evaluations occur during the heavy mathematical processing phase.

```cpp
int sc_main(int argc, char* argv[]) {
    // Signals
    sca_tdf::sca_signal<double> sig_rf("sig_rf");
    sca_tdf::sca_signal<double> sig_lo("sig_lo");
    sca_tdf::sca_signal<double> sig_mixed("sig_mixed");
    sca_tdf::sca_signal<double> sig_downsampled("sig_downsampled");

    // Modules
    Oscillator rf_osc("rf_osc");
    rf_osc.frequency = 2.1e6; // 2.1 MHz
    rf_osc.out(sig_rf);

    Oscillator lo_osc("lo_osc");
    lo_osc.frequency = 2.0e6; // 2.0 MHz
    lo_osc.out(sig_lo);

    Mixer mixer("mixer");
    mixer.rf_in(sig_rf);
    mixer.lo_in(sig_lo);
    mixer.if_out(sig_mixed);

    Decimator dec("dec");
    dec.in(sig_mixed);
    dec.out(sig_downsampled);

    // Tracing
    sca_util::sca_trace_file* tf = sca_util::sca_create_vcd_trace_file("tdf_multirate");
    sca_util::sca_trace(tf, sig_rf, "RF_In");
    sca_util::sca_trace(tf, sig_lo, "LO_In");
    sca_util::sca_trace(tf, sig_mixed, "Mixed_IF");
    sca_util::sca_trace(tf, sig_downsampled, "Downsampled_IF");

    sc_core::sc_start(10.0, sc_core::SC_US);

    sca_util::sca_close_vcd_trace_file(tf);
    return 0;
}
```

### Key LRM Takeaways
- **Timestep Propagation**: You do not need to assign a timestep to every module. The AMS solver automatically propagates timesteps across connected clusters using the consistency equation.
- **Delay Initialization**: If you apply `set_delay(N)` to a port, you must call `.initialize(value, index)` exactly $N$ times within the `initialize()` callback to prime the solver's buffers.
- **Multirate Reads/Writes**: If a port has a rate $> 1$, you must use the indexed version of read/write (e.g., `in.read(i)`) and process exactly the required number of samples during `processing()`.

## Lesson 47: Linear Signal Flow (LSF)

Canonical lesson: https://www.learn-systemc.com/tutorials/046-linear-signal-flow-lsf

Understanding the Linear Signal Flow (LSF) model of computation for modeling continuous-time control loops and filters.

## How to Read This Lesson

# Linear Signal Flow (LSF)

The **Linear Signal Flow (LSF)** model of computation is designed to model continuous-time, non-conservative systems. If you have ever used tools like Simulink to build block diagrams of mathematical transfer functions or PID controllers, you will feel right at home with the LSF MoC in SystemC AMS.

In LSF, a system is modeled as a directed graph where the nodes represent mathematical operations (addition, integration, differentiation, gain) and the edges represent continuous-time real-valued signals.

## Standard and source context

## LSF Fundamentals

Unlike TDF, where you write custom C++ code in a `processing()` callback to define behavior, LSF relies entirely on a library of **predefined primitive modules**. You build your continuous-time equations by instantiating and physically connecting these primitives via signals.

Because LSF represents continuous time, the SystemC AMS solver aggregates all connected LSF primitives into a system of Differential and Algebraic Equations (DAEs). This entire equation system is solved together dynamically during the simulation.

### Core LSF Primitives

The `sca_lsf` namespace provides the necessary primitives to construct your block diagrams. All primitives take inputs from `sca_lsf::sca_in` and drive outputs to `sca_lsf::sca_out` using `sca_lsf::sca_signal` channels.

* **`sca_lsf::sca_add`**: Weighted addition of two signals: $y(t) = k_1 \cdot x_1(t) + k_2 \cdot x_2(t)$
* **`sca_lsf::sca_sub`**: Weighted subtraction: $y(t) = k_1 \cdot x_1(t) - k_2 \cdot x_2(t)$
* **`sca_lsf::sca_gain`**: Multiplies the input by a constant gain: $y(t) = k \cdot x(t)$
* **`sca_lsf::sca_integ`**: Scaled time-domain integration: $y(t) = k \int x(t) dt + y_0$
* **`sca_lsf::sca_dot`**: Scaled time derivative (differentiator).
* **`sca_lsf::sca_ltf_nd`**: Laplace transfer function (Numerator/Denominator coefficients).

### LSF Sources and Sinks

To feed discrete data into the continuous LSF domain, or to read data out of it, you must use converter primitives:
* **`sca_lsf::sca_tdf::sca_source`**: Converts a TDF signal into a continuous LSF signal.
* **`sca_lsf::sca_tdf::sca_sink`**: Samples an LSF signal and converts it back into a discrete TDF signal.

## Complete Example: A Continuous-Time Feedback Loop

The following complete, compilable example demonstrates how to construct a continuous-time low-pass filter using a feedback loop with a subtractor and an integrator. A TDF square wave generator stimulates the filter, and the smoothed continuous response is converted back to TDF for tracing.

```cpp
#include <systemc>
#include <systemc-ams.h>

// 1. A TDF Source generating a Square Wave
SCA_TDF_MODULE(SquareWaveSource) {
    sca_tdf::sca_out<double> out;

    SCA_CTOR(SquareWaveSource) {}

    void set_attributes() {
        set_timestep(1.0, sc_core::SC_MS); // 1 ms discrete timestep
    }

    void processing() {
        // Generate a 1 Hz square wave
        double t = get_time().to_seconds();
        double val = (std::fmod(t, 1.0) < 0.5) ? 1.0 : -1.0;
        out.write(val);
    }
};

// 2. The LSF Continuous-Time Filter
SC_MODULE(LSF_Filter) {
    // Interface to the discrete TDF world
    sca_tdf::sca_in<double>  in;
    sca_tdf::sca_out<double> out;

    // Internal LSF continuous-time signals
    sca_lsf::sca_signal lsf_in_sig;
    sca_lsf::sca_signal lsf_error_sig;
    sca_lsf::sca_signal lsf_out_sig;

    // Converter Primitives
    sca_lsf::sca_tdf::sca_source tdf_to_lsf;
    sca_lsf::sca_tdf::sca_sink   lsf_to_tdf;

    // LSF Math Primitives
    sca_lsf::sca_sub   subtractor;
    sca_lsf::sca_integ integrator;

    SC_CTOR(LSF_Filter)
        : tdf_to_lsf("tdf_to_lsf")
        , lsf_to_tdf("lsf_to_tdf")
        , subtractor("subtractor")
        , integrator("integrator", 10.0) // k_gain = 10.0 for integration
    {
        // 1. Convert TDF input to LSF
        tdf_to_lsf.inp(in);
        tdf_to_lsf.y(lsf_in_sig);

        // 2. Subtractor: Error = Input - Output (Feedback)
        subtractor.x1(lsf_in_sig);
        subtractor.x2(lsf_out_sig); // Feedback loop wired here
        subtractor.y(lsf_error_sig);

        // 3. Integrator: Output = Integral(Error) * 10.0
        integrator.x(lsf_error_sig);
        integrator.y(lsf_out_sig);

        // 4. Convert continuous LSF output back to discrete TDF
        lsf_to_tdf.x(lsf_out_sig);
        lsf_to_tdf.outp(out);
    }
};

int sc_main(int argc, char* argv[]) {
    // Signals
    sca_tdf::sca_signal<double> sig_square("sig_square");
    sca_tdf::sca_signal<double> sig_filtered("sig_filtered");

    // Instantiate Modules
    SquareWaveSource src("src");
    src.out(sig_square);

    LSF_Filter filter("filter");
    filter.in(sig_square);
    filter.out(sig_filtered);

    // Setup Tracing
    sca_util::sca_trace_file* tf = sca_util::sca_create_vcd_trace_file("lsf_filter_wave");
    sca_util::sca_trace(tf, sig_square, "Input_SquareWave");
    sca_util::sca_trace(tf, sig_filtered, "Filtered_Continuous_Response");

    // Start Simulation
    sc_core::sc_start(3.0, sc_core::SC_SEC);

    sca_util::sca_close_vcd_trace_file(tf);
    return 0;
}
```

### Key Takeaways from the LRM

1. **No Processing Callback:** Notice that we did not write a `processing()` function in `LSF_Filter`. The behavior is entirely defined by instantiating primitives (like `sca_sub` and `sca_integ`) and binding their ports to `sca_lsf::sca_signal`s.
2. **Algebraic Loops:** LSF natively handles continuous-time feedback loops. In the example, `lsf_out_sig` is driven by the integrator but is simultaneously fed back into the `x2` port of the subtractor. The AMS solver automatically resolves this loop during the simulation without requiring manual delay insertion (unlike TDF).
3. **Module Hierarchy:** LSF models are wrapped in standard `sc_core::sc_module` classes, not `sca_tdf::sca_module`. This is because LSF primitives are themselves primitive leaf nodes; you use standard SystemC structural binding within `SC_CTOR` to connect them.

## Under the Hood: Symbolic Signals and the Linear Equation Matrix

Unlike TDF or standard SystemC DE where signals literally hold data payloads passing between callbacks, `sca_lsf::sca_signal` operates completely differently in C++.

In the Accellera implementation, an LSF signal is actually a **symbolic node**. When you instantiate `sca_lsf::sca_add` and connect it to a signal, no C++ mathematical functions are registered to execute dynamically. Instead, during the `end_of_elaboration` phase, the AMS Linear Solver parses the netlist and maps every node and primitive into a monolithic state-space mathematical matrix system:

`A * dx(t) + B * x(t) + C * u(t) = 0`

Each primitive simply contributes coefficients to the `A`, `B`, or `C` matrices. For example, an `sca_integ` contributes a differential row to `A`, while an `sca_gain` contributes an algebraic row to `B`.

During simulation, the AMS kernel evaluates the entire LSF cluster simultaneously by stepping through time using numerical integration algorithms (such as Trapezoidal or Gear methods) and solving the matrix via LU decomposition. This means the C++ primitives you connected structurally in your `SC_CTOR` vanish entirely at runtime, replaced by highly optimized internal linear algebra computations.

## Accellera Source Implementation: `sca_linear_solver`

Unlike TDF which uses static scheduling, LSF maps to continuous-time differential equations. In the Accellera AMS source code (e.g., `src/scams/impl/solver/linear/sca_linear_solver.cpp`), the engine constructs a sparse matrix representation of the nodes.

```cpp
// A simplified abstraction of the core AMS linear solver loop
void sca_linear_solver::solve() {
    // 1. Construct Sparse Matrix from LSF/ELN nodes
    build_matrix();

    // 2. Numerical Integration (e.g., Trapezoidal Rule)
    for(int step = 0; step < max_steps; ++step) {
        calculate_derivatives();
        update_node_voltages();

        // 3. Synchronize with Discrete-Event Kernel if needed
        if(crosses_threshold()) {
            sc_core::sc_spawn( [](){ /* notify DE event */ } );
        }
    }
}
```
This is why LSF and ELN components do not have a `processing()` method like TDFâ€”they do not execute sequentially; they are solved simultaneously as a matrix.

## Lesson 48: Electrical Linear Networks (ELN)

Canonical lesson: https://www.learn-systemc.com/tutorials/047-electrical-linear-networks-eln

Modeling conservative continuous-time electrical circuits using the Electrical Linear Networks (ELN) model of computation.

## How to Read This Lesson

# Electrical Linear Networks (ELN)

The **Electrical Linear Networks (ELN)** model of computation brings traditional schematic-based circuit modeling into SystemC AMS. While LSF relies on abstract mathematical data flows, ELN is specifically designed for conservative continuous-time systems. This means the AMS solver automatically enforces **Kirchhoff's Voltage and Current Laws (KVL/KCL)** across the network.

If you are accustomed to SPICE netlists, ELN will look very familiar. You build your model by defining electrical nodes (nets) and connecting physical components (like resistors, capacitors, and voltage sources) between them.

## Standard and source context

## Nodes, Terminals, and Primitives

The core difference between ELN and TDF/LSF is how components interact. In TDF, data flows directionally from an output port to an input port. In ELN, energy is exchanged conservatively across bidirectional terminals.

### 1. Nodes (`sca_eln::sca_node`)
A node represents a physical electrical connection point (a wire or net). The instantaneous voltage at a node is calculated dynamically by the ELN solver relative to a reference ground.
* **`sca_eln::sca_node`**: A standard electrical net.
* **`sca_eln::sca_node_ref`**: The electrical ground (defined as exactly 0 Volts). Every ELN cluster must have at least one path to a reference node.

### 2. Terminals (`sca_eln::sca_terminal`)
ELN components use terminals rather than directional ports. Terminals bind to nodes. By convention, current flows *through* the terminal into the component, and the voltage drops *across* the component's positive (`p`) and negative (`n`) terminals.

### 3. Electrical Primitives
The `sca_eln` namespace provides standard passive electrical components. All of them are instantiated as standard members in an `sc_module`:
* **`sca_eln::sca_r`**: Resistor.
* **`sca_eln::sca_c`**: Capacitor.
* **`sca_eln::sca_l`**: Inductor.
* **`sca_eln::sca_vsource`** / **`sca_eln::sca_isource`**: Independent voltage/current sources.

### Converting to/from ELN
You cannot connect a discrete TDF signal directly to a resistor. Instead, you use TDF-to-ELN converter primitives:
* **`sca_eln::sca_tdf::sca_vsource`**: Acts as an ideal voltage source in the ELN domain, where the instantaneous voltage is driven by the sampled TDF input signal.
* **`sca_eln::sca_tdf::sca_vsink`**: Acts as an ideal voltmeter that reads the voltage drop across two ELN nodes and outputs it as a discrete-time TDF signal.

## Complete Example: An RLC Low-Pass Filter

The following complete, compilable example demonstrates how to construct a classic Resistor-Inductor-Capacitor (RLC) filter. We will drive the filter with a TDF step-signal and read the continuous-time voltage response of the capacitor back into the TDF domain.

```cpp
#include <systemc>
#include <systemc-ams.h>

// 1. TDF Source: Generates a Step input at 1.0 seconds
SCA_TDF_MODULE(StepSource) {
    sca_tdf::sca_out<double> out;

    SCA_CTOR(StepSource) {}

    void set_attributes() {
        set_timestep(1.0, sc_core::SC_MS); // 1 ms discrete timestep
    }

    void processing() {
        double t = get_time().to_seconds();
        double val = (t >= 1.0) ? 5.0 : 0.0; // 0V to 5V Step
        out.write(val);
    }
};

// 2. The ELN RLC Circuit
SC_MODULE(RLC_Circuit) {
    // Interface to the TDF world
    sca_tdf::sca_in<double>  in;
    sca_tdf::sca_out<double> out;

    // Electrical Nodes
    sca_eln::sca_node     n_src; // Node between Source and Resistor
    sca_eln::sca_node     n_rl;  // Node between Resistor and Inductor
    sca_eln::sca_node     n_lc;  // Node between Inductor and Capacitor
    sca_eln::sca_node_ref gnd;   // The electrical ground (0V)

    // Converter Primitives
    sca_eln::sca_tdf::sca_vsource tdf_v_src;
    sca_eln::sca_tdf::sca_vsink   tdf_v_snk;

    // Electrical Primitives
    sca_eln::sca_r resistor;
    sca_eln::sca_l inductor;
    sca_eln::sca_c capacitor;

    SC_CTOR(RLC_Circuit)
        : tdf_v_src("tdf_v_src", 1.0) // Scale = 1.0
        , tdf_v_snk("tdf_v_snk", 1.0)
        , resistor("resistor", 10.0)      // R = 10 Ohms
        , inductor("inductor", 0.1)       // L = 100 mH
        , capacitor("capacitor", 0.001)   // C = 1000 uF
    {
        // 1. Drive the circuit using the TDF input
        tdf_v_src.inp(in);         // Bind to TDF discrete port
        tdf_v_src.p(n_src);        // Positive terminal to Source node
        tdf_v_src.n(gnd);          // Negative terminal to Ground

        // 2. Resistor in series
        resistor.p(n_src);
        resistor.n(n_rl);

        // 3. Inductor in series
        inductor.p(n_rl);
        inductor.n(n_lc);

        // 4. Capacitor to ground
        capacitor.p(n_lc);
        capacitor.n(gnd);

        // 5. Read the voltage across the capacitor (n_lc to ground)
        tdf_v_snk.p(n_lc);
        tdf_v_snk.n(gnd);
        tdf_v_snk.outp(out);       // Bind to TDF discrete port
    }
};

int sc_main(int argc, char* argv[]) {
    // Signals
    sca_tdf::sca_signal<double> sig_step("sig_step");
    sca_tdf::sca_signal<double> sig_response("sig_response");

    // Instantiate Modules
    StepSource src("src");
    src.out(sig_step);

    RLC_Circuit rlc("rlc");
    rlc.in(sig_step);
    rlc.out(sig_response);

    // Setup Tracing
    sca_util::sca_trace_file* tf = sca_util::sca_create_vcd_trace_file("eln_rlc_wave");
    sca_util::sca_trace(tf, sig_step, "Input_Step_Voltage");
    sca_util::sca_trace(tf, sig_response, "Capacitor_Voltage_Response");

    // You can also trace internal ELN nodes directly!
    sca_util::sca_trace(tf, rlc.n_rl, "Node_RL_Voltage");

    // Start Simulation
    sc_core::sc_start(3.0, sc_core::SC_SEC);

    sca_util::sca_close_vcd_trace_file(tf);
    return 0;
}
```

### How the ELN Solver Works

During the elaboration phase, the SystemC AMS kernel identifies all connected ELN primitives (the converters, resistor, inductor, and capacitor) and groups them into an **ELN cluster**. It automatically formulates a continuous-time matrix of differential equations based on KVL and KCL.

During the simulation, whenever the discrete TDF solver injects a new voltage value via `tdf_v_src`, the ELN Linear Solver mathematically integrates the continuous-time response of the RLC circuit across that timestep, tracking the energy stored in the inductor and capacitor. It then provides the precise resulting voltage at node `n_lc` back to the discrete TDF domain via `tdf_v_snk`. This complex analog numerical integration happens entirely under the hood.

## Under the Hood: Modified Nodal Analysis (MNA) Matrices

In C++, an `sca_eln::sca_node` contains no `value` payload. It merely contains a unique integer ID assigned during elaboration. To calculate the voltages across the network, the Accellera AMS ELN solver utilizes **Modified Nodal Analysis (MNA)**.

Under the hood, all `sca_eln` components inherit from an internal base class that provides a virtual `matrix_stamp()` method. The solver allocates global Admittance ($\mathbf{Y}$), Capacitance ($\mathbf{C}$), and Excitation ($\mathbf{J}$) matrices based on the total number of node IDs.

When `sca_eln::sca_r` (Resistor) executes its `matrix_stamp()` during elaboration, it reads its positive node ID ($i$) and negative node ID ($j$), and computes its conductance ($G = 1/R$). It then directly adds $+G$ to the matrix entries $\mathbf{Y}_{i,i}$ and $\mathbf{Y}_{j,j}$, and $-G$ to entries $\mathbf{Y}_{i,j}$ and $\mathbf{Y}_{j,i}$.

Similarly, `sca_eln::sca_c` stamps the $\mathbf{C}$ differential matrix. The solver then computes the continuous-time transient response by numerically inverting these matrices ($\mathbf{x}(t) = \mathbf{Y}^{-1} \cdot \mathbf{J}$) at each solver step. Therefore, when you write `sca_eln::sca_r` in your code, you are not writing a behavioral model; you are instantiating a declarative rule that configures the C++ linear algebra engine.

## Lesson 49: Solver Synchronization and Execution Semantics

Canonical lesson: https://www.learn-systemc.com/tutorials/048-solver-synchronization-and-execution-semantics

Learn how the SystemC AMS solvers synchronize the TDF, LSF, and ELN models of computation with each other and the discrete-event kernel.

## How to Read This Lesson

# Solver Synchronization and Execution Semantics

Because SystemC AMS allows you to mix and match Timed Data Flow (TDF), Linear Signal Flow (LSF), Electrical Linear Networks (ELN), and standard SystemC discrete-event (DE) models, it relies on a sophisticated synchronization layer to ensure time remains consistent across all domains.

In this tutorial, we will explore how these different solvers interact, how time steps are propagated, and how data moves securely between the analog and digital worlds.

## Standard and source context

## TDF as the Master Scheduler

In SystemC AMS, the **TDF solver** acts as the primary time-keeper for the continuous-time domains.

When you build an LSF or ELN model, those models form a system of continuous-time equations. However, a computer cannot simulate continuous time infinitely; it must discretize it. The LSF and ELN solvers derive their calculation timesteps directly from the TDF cluster they are connected to.

If you connect an ELN circuit to a TDF module that runs with a `1.0 us` timestep, the ELN solver will simulate that circuit in chunks of `1.0 us` to provide a synchronized output back to the TDF module.

## Synchronizing with the SystemC DE Kernel

Ultimately, your AMS system will likely need to communicate with a standard SystemC discrete-event digital component (like a processor, an interrupt controller, or a TLM bus).

Synchronization between the AMS solvers and the SystemC DE kernel is done exclusively through specialized converter ports:
* **`sca_tdf::sca_de::sca_in<T>`**: Reads a standard `sc_core::sc_signal<T>` from the DE kernel into the TDF domain.
* **`sca_tdf::sca_de::sca_out<T>`**: Writes a TDF sample to a standard `sc_core::sc_signal<T>` in the DE kernel.

### The Synchronization Rules

According to the IEEE 1666.1 LRM:
1. **Reading from DE to TDF**: When the TDF solver reads a value from a `sca_de::sca_in` port during a `processing()` callback, it reads the value that was present on the SystemC signal at the **first delta cycle** of the current SystemC simulation time. The value is assumed to remain constant for the duration of the TDF timestep.
2. **Writing from TDF to DE**: When the TDF solver writes a sample to a `sca_de::sca_out` port, the value is written to the SystemC signal at the exact corresponding SystemC time, triggering standard SystemC update phases and events (like `value_changed_event()`).

## Under the Hood: The Converter Ports and the Update Phase

How does `sca_tdf::sca_de::sca_out` safely inject continuous mathematical data into a discrete digital event queue without causing race conditions?

Inside the Accellera implementation, `sca_de::sca_out` contains a pointer to the bound `sc_core::sc_signal`. During the TDF `processing()` phase, calling `analog_out.write(voltage)` does not immediately trigger any SystemC events. Instead, the AMS solver buffers the requested data alongside its calculated timestamp.

When the TDF cluster finishes executing its static schedule for the current cluster period, the internal `SC_THREAD` managing that cluster actively calls `m_bound_sc_signal->write(value)`. In SystemC, `.write()` merely places an update request into the DE kernel's evaluation queue.
The AMS cluster thread then explicitly calls `sc_core::wait()` to suspend itself. This action hands control back to the SystemC DE kernel, which immediately runs the **Update Phase**. The SystemC kernel safely resolves the `sc_signal` writes, triggers the `value_changed_event()`, and wakes up any purely digital modules (like the `DigitalMonitor` below) in the subsequent delta cycle. This carefully orchestrated thread yield guarantees thread-safety and causality between the analog equations and digital logic.

## Complete Example: DE and TDF Synchronization

Here is a complete, compilable example demonstrates a TDF module generating a mathematical waveform, connected via a converter port to a purely digital SystemC module (a simple threshold monitor) that wakes up only when the analog value crosses a boundary.

```cpp
#include <systemc>
#include <systemc-ams.h>

// 1. TDF Domain: Analog Waveform Generator
SCA_TDF_MODULE(AnalogSensor) {
    // A converter port: TDF driving a standard SystemC DE signal
    sca_tdf::sca_de::sca_out<double> analog_out;

    SCA_CTOR(AnalogSensor) {}

    void set_attributes() {
        set_timestep(1.0, sc_core::SC_MS); // 1 millisecond sampling
    }

    void processing() {
        // Generate a slow 1 Hz sine wave
        double t = get_time().to_seconds();
        double voltage = 5.0 * std::sin(2.0 * M_PI * 1.0 * t);

        // Write to the DE kernel. This schedules a SystemC event.
        analog_out.write(voltage);
    }
};

// 2. Digital Domain: Discrete-Event Monitor
SC_MODULE(DigitalMonitor) {
    sc_core::sc_in<double> analog_in;

    SC_CTOR(DigitalMonitor) {
        SC_THREAD(monitor_thread);
        // Wake up whenever the analog signal changes
        sensitive << analog_in.value_changed_event();
        dont_initialize();
    }

    void monitor_thread() {
        bool threshold_exceeded = false;

        while (true) {
            double current_val = analog_in.read();

            if (current_val > 4.5 && !threshold_exceeded) {
                threshold_exceeded = true;
                std::cout << "@ " << sc_core::sc_time_stamp()
                          << " [DIGITAL ALARM]: Voltage exceeded 4.5V! (Value: "
                          << current_val << "V)\n";
            }
            else if (current_val < 4.0 && threshold_exceeded) {
                threshold_exceeded = false;
                std::cout << "@ " << sc_core::sc_time_stamp()
                          << " [DIGITAL CLEAR]: Voltage dropped below 4.0V.\n";
            }

            wait(); // Wait for the next value_changed_event
        }
    }
};

int sc_main(int argc, char* argv[]) {
    // 3. The boundary signal: Standard SystemC sc_signal
    sc_core::sc_signal<double> sig_analog_voltage("sig_analog_voltage");

    // 4. Instantiate and bind
    AnalogSensor sensor("sensor");
    sensor.analog_out(sig_analog_voltage); // TDF writes to DE

    DigitalMonitor monitor("monitor");
    monitor.analog_in(sig_analog_voltage); // DE reads from DE

    // Setup Tracing to observe the synchronization
    sca_util::sca_trace_file* tf = sca_util::sca_create_vcd_trace_file("sync_wave");
    sca_util::sca_trace(tf, sig_analog_voltage, "Sensor_Voltage");

    // Run simulation for 2 seconds
    std::cout << "Starting mixed-signal simulation...\n";
    sc_core::sc_start(2.0, sc_core::SC_SEC);

    sca_util::sca_close_vcd_trace_file(tf);
    return 0;
}
```

### Event-Driven TDF Activation (Dynamic TDF)

In Dynamic TDF (introduced in SystemC AMS 2.0), a TDF module can actually suspend its static schedule and wait for a discrete-event trigger.

By using `request_next_activation(port.default_event())` inside the `processing()` callback, a TDF module can wake up reactively when a digital signal changes. This saves immense computation power when the analog domain is otherwise idle, rather than forcing the TDF solver to calculate empty samples on a fixed timestep.

## Lesson 50: AMS TDF Rates, Delays, and Timesteps

Canonical lesson: https://www.learn-systemc.com/tutorials/049-ams-tdf-rates-delays-and-timesteps

How Timed Data Flow modules communicate through rates, delays, timesteps, sample timing, and solver scheduling.

## How to Read This Lesson

# AMS TDF Rates, Delays, and Timesteps

The Timed Data Flow (TDF) model of computation is highly efficient because it relies on static scheduling. The solver does not execute TDF modules dynamically based on hardware events; instead, it pre-calculates an execution order based on **Rates**, **Delays**, and **Timesteps**.

Understanding the mathematical relationship between these three attributes is the most important skill for an AMS engineer.

## Standard and source context

## The Consistency Equation

The fundamental law of TDF is the consistency equation:
**$T_m = T_p \times R$**

- **$T_m$ (Module Timestep)**: The physical time interval between two consecutive executions of a module's `processing()` callback.
- **$T_p$ (Port Timestep)**: The physical time interval between two consecutive samples arriving at (or leaving) a port.
- **$R$ (Rate)**: The number of samples read or written per `processing()` activation.

In code and in the LRM, these timing values are represented with the AMS time vocabulary around `sca_core::sca_time` / `sca_time`, not as an ordinary `double` that happens to mean nanoseconds. That distinction matters in reviews: the value carries simulation-time units, participates in timestep negotiation, and eventually has to synchronize with `sc_core::sc_time` at the discrete-event boundary.

If a module executes every 10 ns ($T_m = 10$), and it writes 2 samples per execution ($R = 2$), the port timestep ($T_p$) MUST be 5 ns. The AMS solver automatically propagates these timesteps across the cluster. If it detects a mathematical contradiction (e.g., a 5 ns port connected to a 4 ns port without a rate conversion), it will halt elaboration with a fatal error.

## Algebraic Loops and Delay

If you connect two TDF modules in a feedback loop (Module A drives Module B, and Module B drives Module A), the static scheduler cannot determine which module should execute first. This is called an **algebraic loop**.

To break an algebraic loop, you must explicitly insert a **Delay** using `set_delay(N)` on one of the ports. A delay of $N$ means that the port outputs $N$ initial samples before producing the first calculated sample. This acts like a $z^{-1}$ mathematical register, decoupling the dependency chain and allowing the solver to schedule the modules.

If you set a delay of $N$, you **must** implement the `initialize()` callback and initialize exactly $N$ samples on that port.

## Complete Example: Fibonacci Feedback Loop

The following complete, compilable `sc_main` example demonstrates a classic algebraic loop: a Fibonacci sequence generator. It requires a feedback loop to add the previous two numbers, which forces us to use `set_delay` and `initialize`.

```cpp
#include <systemc>
#include <systemc-ams.h>

// 1. A TDF Adder with a Delay to break the algebraic loop
SCA_TDF_MODULE(FibonacciAdder) {
    sca_tdf::sca_in<int>  in_prev;
    sca_tdf::sca_out<int> out_next;

    SCA_CTOR(FibonacciAdder) {}

    void set_attributes() {
        set_timestep(1.0, sc_core::SC_MS); // Execute every 1 ms

        // We are generating a feedback loop.
        // We must delay the output by 1 sample to break the algebraic loop.
        // This effectively turns the output into a mathematical register (z^-1).
        out_next.set_delay(1);
    }

    void initialize() {
        // Because we declared a delay of 1, we MUST provide exactly 1 initial sample.
        // In the Fibonacci sequence, we start with 1.
        out_next.initialize(1, 0); // Initialize value 1 at index 0
    }

    void processing() {
        // Read the previous state from the feedback loop
        int prev_val = in_prev.read();

        // We need to keep track of the n-2 state internally to calculate the next
        static int n_minus_2 = 0;

        // Calculate the next Fibonacci number
        int next_val = prev_val + n_minus_2;

        // Update state
        n_minus_2 = prev_val;

        // Write the next value
        out_next.write(next_val);

        std::cout << "@ " << sc_core::sc_time_stamp() << " : " << next_val << "\n";
    }
};

// 2. A simple pass-through to complete the loop
SCA_TDF_MODULE(FeedbackPath) {
    sca_tdf::sca_in<int>  in;
    sca_tdf::sca_out<int> out;

    SCA_CTOR(FeedbackPath) {}

    void set_attributes() {
        // No explicit timestep needed; it propagates from FibonacciAdder
    }

    void processing() {
        // Simply pass the data back to the adder
        out.write(in.read());
    }
};

int sc_main(int argc, char* argv[]) {
    // Signals
    sca_tdf::sca_signal<int> sig_forward("sig_forward");
    sca_tdf::sca_signal<int> sig_feedback("sig_feedback");

    // Instantiate Modules
    FibonacciAdder adder("adder");
    adder.in_prev(sig_feedback);
    adder.out_next(sig_forward);

    FeedbackPath path("path");
    path.in(sig_forward);
    path.out(sig_feedback);

    std::cout << "Starting Fibonacci Generator...\n";

    // Run for 15 milliseconds (will generate 15 numbers)
    sc_core::sc_start(15.0, sc_core::SC_MS);

    return 0;
}
```

### Best Practice

Keep TDF modules side-effect-light. Treat them strictly as signal processing blocks with explicit sample timing. If you need dynamic control logic (like starting or stopping the filter based on external interrupts), use standard SystemC discrete-event modules at the boundary to orchestrate the TDF cluster.

## Under the Hood: Topological Sorting and the Execution Array

Why exactly does an algebraic loop cause a simulation failure, and how does `set_delay(N)` fix it under the hood?

During the `end_of_elaboration` phase, the SystemC AMS kernel constructs a bipartite Directed Acyclic Graph (DAG) for every TDF cluster. In this graph, modules are one type of node, and signals are the other. Directed edges represent data flowing from module output ports to signals, and from signals to module input ports.

To generate the static execution schedule, the kernel performs a classic topological sort on this DAG. The sorting algorithm ensures that a module is only scheduled to execute *after* all the modules that drive its inputs have executed. If a feedback loop exists, a cycle is present in the graph. The topological sort detects the cycle, fails, and throws a fatal algebraic loop error because it cannot find a valid starting node.

When you call `set_delay(1)` on a port, you are explicitly altering the graph connectivity. The kernel interprets the delayed port not as a standard edge, but as an independent initial-condition source (a buffer pre-filled during `initialize()`). This conceptually "cuts" the dependency edge in the graph, removing the cycle. The topological sort can then successfully traverse the tree and flatten it into a 1D C++ array of `sca_module*` pointers. During the `processing()` phase, the cluster's synchronization thread simply loops over this flat array in zero time, executing `ptr->processing()` with maximum efficiency.

## Lesson 51: AMS Converter Ports and Domain Boundaries

Canonical lesson: https://www.learn-systemc.com/tutorials/050-ams-converter-ports-and-domain-boundaries

How SystemC discrete-event models connect to AMS TDF models through converter ports and disciplined boundary design.

## How to Read This Lesson

# AMS Converter Ports and Domain Boundaries

Mixed-signal modeling is mostly about managing boundaries. A digital controller, a TLM register block, or a standard SystemC `SC_THREAD` inevitably needs to interact with an AMS dataflow model.

If you connect them casually, the model becomes confusing and simulation performance degrades. A clean boundary explicitly defines:
- How often analog values are sampled.
- How continuous/sampled analog crossings become digital discrete events.
- Which domain owns the timing.

## Standard and source context

## The Converter Ports

To cross between the SystemC Discrete-Event (DE) kernel and the AMS TDF solver, the LRM mandates the use of converter ports:

- **`sca_tdf::sca_de::sca_in<T>`**: TDF reads a DE `sc_signal`.
- **`sca_tdf::sca_de::sca_out<T>`**: TDF writes to a DE `sc_signal`.

> [!WARNING]
> A TDF output port driving a discrete-event signal can produce an event storm. If a TDF module running at a 1 ns timestep continuously writes slightly fluctuating analog values to a DE signal, the SystemC kernel will wake up millions of times per second, destroying your simulation performance. You should always use a comparator or threshold detector in the AMS domain to reduce the event frequency before crossing the boundary into DE.

## Complete Example: Smart Temperature Sensor

The following complete `sc_main` example perfectly illustrates mixed-signal boundaries. It models a temperature sensor. The sensor's raw physics (a TDF waveform) is processed by a TDF Threshold Comparator. Only when the temperature crosses a dangerous threshold does the TDF module output a boolean event into the SystemC Digital domain, triggering an interrupt.

```cpp
#include <systemc>
#include <systemc-ams.h>

// 1. TDF Domain: Analog Temperature Physics
SCA_TDF_MODULE(AnalogTempPhysics) {
    sca_tdf::sca_out<double> temp_out;

    SCA_CTOR(AnalogTempPhysics) {}

    void set_attributes() {
        set_timestep(10.0, sc_core::SC_MS); // Sample physics every 10 ms
    }

    void processing() {
        // Simulate a slow temperature rise (e.g., an engine heating up)
        double t = get_time().to_seconds();
        double temperature = 20.0 + (t * 5.0); // Starts at 20C, rises 5C per second
        temp_out.write(temperature);
    }
};

// 2. TDF Domain: Analog Threshold Comparator
// This module prevents "event storms" by only outputting boolean state changes.
SCA_TDF_MODULE(ThresholdComparator) {
    sca_tdf::sca_in<double> temp_in;

    // Converter Port: TDF driving standard SystemC DE
    sca_tdf::sca_de::sca_out<bool> alarm_out;

    double threshold;

    SCA_CTOR(ThresholdComparator) : threshold(75.0) {}

    void set_attributes() {
        // Inherits timestep from AnalogTempPhysics (10 ms)
    }

    void processing() {
        double current_temp = temp_in.read();

        // Write the boolean evaluation.
        // Note: Writing the exact same boolean value repeatedly to a SystemC sc_signal
        // does NOT trigger value_changed_event() continuously, saving performance.
        alarm_out.write(current_temp >= threshold);
    }
};

// 3. Digital Domain: SystemC Interrupt Controller
SC_MODULE(InterruptController) {
    // Standard SystemC DE port
    sc_core::sc_in<bool> hw_alarm;

    SC_CTOR(InterruptController) {
        SC_THREAD(monitor_interrupts);
        // Only wake up when the boolean alarm state changes
        sensitive << hw_alarm.value_changed_event();
        dont_initialize();
    }

    void monitor_interrupts() {
        while(true) {
            if (hw_alarm.read() == true) {
                std::cout << "@ " << sc_core::sc_time_stamp()
                          << " [DIGITAL_CTRL]: HARDWARE ALARM TRIGGERED! Shutting down system.\n";
            } else {
                std::cout << "@ " << sc_core::sc_time_stamp()
                          << " [DIGITAL_CTRL]: Alarm cleared. System nominal.\n";
            }
            wait();
        }
    }
};

int sc_main(int argc, char* argv[]) {
    // Mixed-Signal Boundary Signals
    sca_tdf::sca_signal<double> sig_temp("sig_temp"); // TDF-to-TDF
    sc_core::sc_signal<bool> sig_alarm("sig_alarm");  // TDF-to-DE

    // Instantiate Modules
    AnalogTempPhysics physics("physics");
    physics.temp_out(sig_temp);

    ThresholdComparator comparator("comparator");
    comparator.temp_in(sig_temp);
    comparator.alarm_out(sig_alarm);

    InterruptController ctrl("ctrl");
    ctrl.hw_alarm(sig_alarm);

    // Setup Tracing
    sca_util::sca_trace_file* tf = sca_util::sca_create_vcd_trace_file("boundary_wave");
    sca_util::sca_trace(tf, sig_temp, "Analog_Temperature");
    sca_util::sca_trace(tf, sig_alarm, "Digital_Alarm_Signal");

    // Start Simulation
    std::cout << "Starting Mixed-Signal Simulation...\n";
    sc_core::sc_start(15.0, sc_core::SC_SEC); // Simulate 15 seconds of physics

    sca_util::sca_close_vcd_trace_file(tf);
    return 0;
}
```

### Best Practices for Modeler

1. **Minimize Crossings**: Put analog-like signal processing (filters, integrations) entirely inside AMS clusters. Put digital control (state machines, registers) entirely inside SystemC modules.
2. **Compress Data**: Use comparators or decimation filters inside the TDF domain to reduce the rate of data crossing into the DE domain.
3. **Document Timing Ownership**: Clearly document which module acts as the timing master. In the example above, the TDF `AnalogTempPhysics` module sets the timestep, dictating exactly when the `ThresholdComparator` runs and when the DE boundary is evaluated.

## Under the Hood: The `sc_signal` Equality Optimization

Why is it so crucial to use a `ThresholdComparator` rather than driving raw doubles into the DE domain? The answer lies in the C++ implementation of `sc_core::sc_signal<T>::write(const T& value)`.

When the TDF converter port writes a value to a bound DE signal, the SystemC kernel executes an equality check: `if (value == m_new_val) return;`. If the value hasn't changed, the function returns immediately without scheduling an update request or triggering the `value_changed_event()`.

If you were to route the raw continuous `temperature` (a double-precision float) directly into an `sc_signal<double>`, the value would be slightly different at every 10 ms timestep. The equality check would always fail, and the SystemC DE kernel would be forced to context switch, evaluate the signal, and wake up any sensitive digital threads 100 times a second.

By evaluating the threshold inside the TDF domain and driving an `sc_signal<bool>`, the TDF module repeatedly writes `false` to the port. The SystemC kernel detects `false == false`, instantly drops the write request, and allows the simulation to remain almost entirely within the high-speed, static TDF loop. The heavy DE event machinery only activates at the exact delta cycle where the boolean flips to `true`.

## Lesson 52: AMS Modeling Style for Virtual Platforms

Canonical lesson: https://www.learn-systemc.com/tutorials/051-ams-modeling-style-for-virtual-platforms

How to combine SystemC AMS with a VP without turning a software platform model into a slow analog simulation.

## How to Read This Lesson

# AMS Modeling Style for Virtual Platforms

SystemC AMS can enrich a Virtual Platform (VP), but it should not accidentally change the purpose of the VP. Virtual platforms are primarily designed for fast software execution, whereas analog simulations often require fine-grained time steps that slow down simulation significantly.

## Standard and source context

## When AMS Belongs in a VP

According to the SystemC AMS standard (IEEE 1666.1), you should use AMS when software-visible behavior depends on analog or signal-processing effects:

- ADC sample streams
- Sensor thresholds
- PLL lock approximation
- Power or thermal trends
- Filters
- Motor-control feedback

Do not use AMS merely to make the model look more advanced.

## Good VP Boundary

A practical mixed VP usually has:

- A TLM register block for software programming.
- An AMS cluster (TDF or LSF) for signal behavior.
- Converter ports (e.g., `sca_tdf::sca_in`, `sca_tdf::sca_out` connected to `sc_core::sc_signal`) for control and status between the AMS and discrete-event worlds.
- Interrupts or status registers for software-visible results.

The software should still see registers, memory, and interrupts, modeled via standard TLM-2.0 or Simple Bus abstraction.

## Complete AMS to SystemC Boundary Example

Below is a fully compilable `sc_main` program demonstrating how to interface an AMS TDF (Timed Data Flow) model representing an ADC with a standard SystemC discrete-event module representing a simple Virtual Platform peripheral register block.

```cpp
#include <systemc>
#include <systemc-ams>

// 1. AMS TDF Module (The Analog / Continuous Part)
SCA_TDF_MODULE(adc_sensor) {
    // Converter port: Continuous output to Discrete Event (DE) domain
    sca_tdf::sca_out<double> analog_out;

    // Internal state
    double current_val;

    SCA_CTOR(adc_sensor) : analog_out("analog_out"), current_val(0.0) {}

    void set_attributes() override {
        // Set a coarse timestep so we don't slow down the VP unnecessarily
        set_timestep(1.0, sc_core::SC_MS);
    }

    void processing() override {
        // Simulate a slowly changing analog value (e.g., a temperature sensor)
        current_val += 0.5;
        if (current_val > 100.0) {
            current_val = 0.0;
        }
        // Write out the analog value to the converter port
        analog_out.write(current_val);
    }
};

// 2. VP Peripheral (The Discrete Event / Software Visible Part)
SC_MODULE(vp_adc_peripheral) {
    // Input from the AMS domain
    sc_core::sc_in<double> analog_in;

    // Interrupt output to the CPU
    sc_core::sc_out<bool> irq_out;

    // A simple threshold register programmable by software
    double threshold_reg;

    SC_CTOR(vp_adc_peripheral) : analog_in("analog_in"), irq_out("irq_out"), threshold_reg(50.0) {
        SC_METHOD(monitor_threshold);
        sensitive << analog_in; // Trigger whenever the AMS converter writes a new value
        dont_initialize();
    }

    void monitor_threshold() {
        double current = analog_in.read();

        // If the analog value crosses the software-programmed threshold, trigger an interrupt
        if (current >= threshold_reg) {
            irq_out.write(true);
            std::cout << "@ " << sc_core::sc_time_stamp()
                      << " VP ADC: Threshold crossed! Value: " << current
                      << ", raising IRQ." << std::endl;
        } else {
            irq_out.write(false);
        }
    }
};

// 3. Top-Level Integration
int sc_main(int argc, char* argv[]) {
    // Signals
    sc_core::sc_signal<double> analog_sig("analog_sig");
    sc_core::sc_signal<bool> irq_sig("irq_sig");

    // Instantiation
    adc_sensor ams_block("ams_block");
    vp_adc_peripheral vp_block("vp_block");

    // Binding
    ams_block.analog_out(analog_sig);
    vp_block.analog_in(analog_sig);
    vp_block.irq_out(irq_sig);

    // Run simulation
    std::cout << "Starting mixed AMS/VP simulation..." << std::endl;
    sc_core::sc_start(200.0, sc_core::SC_MS);
    std::cout << "Simulation finished." << std::endl;

    return 0;
}
```

## Performance Rule

Keep AMS timesteps as coarse as the use case allows. A VP that needs to boot firmware should not spend most of its time solving unnecessary high-resolution analog detail. In the example above, a `1.0 ms` timestep was used to minimize context switches between the AMS solver and the SystemC kernel.

## Under the Hood: Dynamic TDF (`request_next_activation`)

In standard TDF, the internal `SC_THREAD` loops rigidly at `cluster_period` intervals. Even if the analog signal isn't changing or the software isn't reading the ADC, the SystemC scheduler is continually interrupted to execute the solver matrix.

To optimize VPs, the Accellera implementation of SystemC AMS 2.0 introduced **Dynamic TDF**. Inside your `processing()` callback, you can explicitly call the C++ method `request_next_activation(sc_event)`.

When you make this call, you are signaling the AMS cluster thread to abandon its fixed static schedule for this module. The cluster thread internally yields and does not automatically wake up on the next timestep. Instead, it waits indefinitely until the standard `sc_core::sc_event` is fired.

For example, if a TLM-2.0 `b_transport` read arrives from the digital CPU, the TLM socket can fire an `sc_event`. The TDF module wakes up, computes a single analog sample, provides the result back to the TLM thread, and goes back to sleep. This "demand-driven" analog evaluation means the AMS solver consumes zero CPU cycles while the software VP is executing unrelated code, drastically accelerating boot times.

## Documentation Rule

For each AMS block in a VP, document:

- Model of computation (e.g., TDF, LSF, ELN)
- Timestep and rates
- Delays
- Boundary ports (Converter type)
- Numerical approximations
- Software-visible effects (IRQs, specific register behaviors)

This lets users understand what is accurate, approximate, and intentionally ignored.

## Lesson 53: AC and Noise Analysis in SystemC AMS

Canonical lesson: https://www.learn-systemc.com/tutorials/052-ac-and-noise-analysis-in-systemc-ams

A deep dive into small-signal AC and noise analysis frequency-domain sweeps in SystemC AMS 2.0.

## How to Read This Lesson

# AC and Noise Analysis in SystemC AMS

SystemC AMS 2.0 (IEEE 1666.1) standardized **Small-Signal Frequency-Domain (AC) and Noise Analysis**. Unlike traditional digital simulators that are strictly tied to the time domain, SystemC AMS allows you to perform exact, swept-frequency AC and Noise analyses on your abstract analog models.

This opens the door for system-level architects to verify frequency responses (like Bode plots, filter cut-offs, or amplifier bandwidths) and noise figures of abstract mixed-signal topologies before moving to SPICE or Verilog-A.

## Standard and source context

## The Kernel Reality: The AC Solver Bypass

When you execute an AC sweep in SystemC AMS, you completely bypass the standard SystemC discrete-event kernel. If you look at the Accellera SystemC AMS source code, calling `sca_ac_analysis::sca_ac_start()` invokes a specialized `sca_ac_domain_solver`.

Unlike time-domain simulations that execute the `processing()` callback continuously via delta cycles and time jumps, the `sca_ac_domain_solver` loops over the specified frequency range. At each frequency step, it calls the **`ac_processing()`** callback of every registered TDF module precisely once to construct and solve a steady-state complex linear equation matrix (`A * x = b`).

If a module lacks an `ac_processing()` implementation, the simulator typically assumes its AC output values are zero, making it an open circuit for AC analysis.

Inside `ac_processing()`, you do **not** write time-domain equations. Instead, you describe the complex, linear relationship between the inputs and the outputs at a given frequency.

### Key `sca_ac_analysis` Utilities

When inside `ac_processing()`, you will heavily rely on the `sca_ac_analysis` namespace.
Here are the critical tools that interface with the internal `sca_ac_signal` views:
- **`sca_ac(port)`**: Used to read a complex value from an input port or write a complex contribution to an output port.
- **`sca_ac_w()`**: Returns the current angular frequency ($\omega$) in radians per second.
- **`sca_ac_s()`**: Returns the Laplace complex frequency variable ($s = j\omega$).
- **`sca_ac_z()`**: Returns the Z-domain complex frequency variable.
- **`sca_ac_noise(noise_density, "name")`**: Injects frequency-dependent noise into the port. Under the hood, this registers an independent `sca_noise_source` in the solver matrix, calculating power spectral density contributions automatically.

## Running an AC Sweep in `sc_main()`

To trigger an AC analysis, you use **`sca_ac_analysis::sca_ac_start()`** and **`sca_ac_analysis::sca_ac_noise_start()`** within `sc_main()`. These functions are highly configurable, allowing you to sweep frequencies linearly or logarithmically.

You also use standard tabular trace files (`sca_util::sca_create_tabular_trace_file`), but rather than tracing time, they will automatically trace the frequency axis.

## Complete End-to-End Example

Below is a 100% complete and compilable example of a first-order Low-Pass Filter (LPF) implemented in TDF. The model defines both a time-domain `processing()` function and an `ac_processing()` function for AC/Noise sweeps.

```cpp
#include <systemc-ams>

// 1. TDF Module for a Low-Pass Filter
SCA_TDF_MODULE(low_pass_filter) {
    sca_tdf::sca_in<double>  in;
    sca_tdf::sca_out<double> out;

    double f_cut; // Cut-off frequency

    SCA_CTOR(low_pass_filter) : in("in"), out("out"), f_cut(1.0e3) {}

    void set_attributes() {
        set_timestep(1.0, sc_core::SC_US); // 1 MHz sampling in time domain
    }

    void initialize() {
        // Laplace transfer function initialization for time-domain
        num(0) = 1.0;
        den(0) = 1.0;
        den(1) = 1.0 / (2.0 * M_PI * f_cut);
    }

    // Time-domain processing
    void processing() {
        // Use the pre-defined continuous-time Laplace Transfer Function in TDF
        out.write( ltf_nd(num, den, in.read()) );
    }

    // AC and Noise frequency-domain processing
    void ac_processing() {
        // Retrieve the current Laplace variable s = j * w
        // Provided dynamically by the sca_ac_domain_solver per frequency step
        sca_util::sca_complex s = sca_ac_analysis::sca_ac_s();

        // Compute transfer function: H(s) = 1 / (1 + s / w_cut)
        double w_cut = 2.0 * M_PI * f_cut;
        sca_util::sca_complex H = 1.0 / (1.0 + s / w_cut);

        // Calculate AC response
        sca_util::sca_complex out_ac = H * sca_ac_analysis::sca_ac(in);

        // Add an arbitrary thermal noise floor (e.g., 1 nV / sqrt(Hz))
        sca_util::sca_complex noise = sca_ac_analysis::sca_ac_noise(1.0e-9, "thermal_noise");

        // Write the complex result out
        sca_ac_analysis::sca_ac(out) = out_ac + noise;
    }

private:
    sca_util::sca_vector<double> num;
    sca_util::sca_vector<double> den;
};

// 2. Simple AC Stimulus
SCA_TDF_MODULE(ac_source) {
    sca_tdf::sca_out<double> out;

    SCA_CTOR(ac_source) : out("out") {}

    void set_attributes() {
        set_timestep(1.0, sc_core::SC_US);
    }

    void processing() {
        // Time domain stimulus (e.g., DC bias or pulse)
        out.write(1.0);
    }

    void ac_processing() {
        // Drive a 1V small-signal AC amplitude
        sca_ac_analysis::sca_ac(out) = sca_util::sca_complex(1.0, 0.0);
    }
};

// 3. System execution
int sc_main(int argc, char* argv[]) {
    // Signals
    sca_tdf::sca_signal<double> sig_in("sig_in");
    sca_tdf::sca_signal<double> sig_out("sig_out");

    // Instantiation
    ac_source src("src");
    src.out(sig_in);

    low_pass_filter lpf("lpf");
    lpf.in(sig_in);
    lpf.out(sig_out);
    lpf.f_cut = 10.0e3; // 10 kHz cutoff

    // Setup frequency-domain tracing
    sca_util::sca_trace_file* ac_tf = sca_util::sca_create_tabular_trace_file("ac_results");

    // Trace complex outputs (SystemC AMS will log magnitude and phase automatically)
    sca_ac_analysis::sca_trace(ac_tf, sig_in, "input_node");
    sca_ac_analysis::sca_trace(ac_tf, sig_out, "output_node");

    std::cout << "Starting AC Analysis..." << std::endl;
    // Sweep from 1 Hz to 1 MHz, 100 points per decade, Logarithmic
    // Bypasses time-domain evaluation completely!
    sca_ac_analysis::sca_ac_start(1.0, 1.0e6, 100, sca_ac_analysis::SCA_LOG);

    std::cout << "Starting AC Noise Analysis..." << std::endl;
    // Sweep Noise over the same range
    sca_ac_analysis::sca_ac_noise_start(1.0, 1.0e6, 100, sca_ac_analysis::SCA_LOG);

    sca_util::sca_close_tabular_trace_file(ac_tf);

    // Note: sc_start() is not explicitly required if we only want AC analysis,
    // but you can call it to run time-domain simulations afterwards.
    // sc_core::sc_start(1.0, sc_core::SC_MS);

    return 0;
}
```

### Analysis of the Run
1. When `sca_ac_start` is called, the SystemC AMS kernel elaborates the model and evaluates `ac_processing()` across the defined frequency sweep.
2. `sca_ac(out) = H * sca_ac_analysis::sca_ac(in);` dynamically scales the input source by the filter's transfer function at every frequency step.
3. The results are dumped into `ac_results.dat` with frequency as the X-axis, allowing you to plot a perfect Bode diagram of your architecture.

## Lesson 54: Dynamic TDF in SystemC AMS

Canonical lesson: https://www.learn-systemc.com/tutorials/053-dynamic-tdf-in-systemc-ams

A deep dive into Dynamic TDF (multirate varying timesteps and dynamic activation) in SystemC AMS 2.0.

## How to Read This Lesson

# Dynamic TDF in SystemC AMS

The original Timed Data Flow (TDF) model in SystemC AMS 1.0 was strictly static. Rates, delays, and timesteps were negotiated during elaboration and locked in place. This made simulation blazing fast, but made it exceptionally difficult to model systems with dynamic timing behaviorâ€”such as Pulse Width Modulation (PWM), event-driven state machines, clock jitters, and variable-frequency drives.

SystemC AMS 2.0 (IEEE 1666.1) introduced **Dynamic TDF**, which breaks the static scheduling paradigm. It allows a TDF module to **dynamically request its next activation time**, reacting instantly to external discrete events (DE) or internally calculated timelines.

## Standard and source context

## The Kernel Reality: Cluster Synchronization

To understand Dynamic TDF, you must look at the Accellera SystemC AMS cluster scheduling algorithm. In static TDF, the kernel builds a fixed execution list (`sca_cluster_synchronization`). Every module runs sequentially in an unrolled `for` loop.

When you opt-in to Dynamic TDF, you break this assumption. The kernel must now dynamically rebuild the cluster execution graph at runtime.
If Module A requests a 2ms timestep, but it is connected to Module B which is forced to run at 1ms, the `sca_sync_value_provider` in the kernel must reconcile these differences. It does this by aggressively selecting the **earliest requested time** across the entire cluster, forcing all interconnected modules to execute early to guarantee data causality.

This dynamic graph recalculation incurs a performance penalty, which is why Dynamic TDF is strictly opt-in.

### 1. `set_attributes()` Configuration
Inside the `set_attributes()` callback, you use the following methods:
- **`does_attribute_changes()`**: The module declares that it will dynamically alter its timing or rates.
- **`accept_attribute_changes()`**: The module declares it can safely participate in a cluster where another module dynamically alters the schedule.

### 2. The `change_attributes()` Callback
Dynamic TDF introduces a completely new callback: **`change_attributes()`**.
This callback is executed by the kernel's attribute evaluation phase *immediately before* the `processing()` phase. Inside this callback, you calculate the exact time the module should run next, overriding the static timestep.

### 3. `request_next_activation()`
Inside `change_attributes()`, you use **`request_next_activation()`** to notify the kernel's `sca_cluster_synchronization`:
- `request_next_activation(sc_core::sc_time)`: Schedule relative to the current time. The kernel internally schedules a dynamic `sc_event`.
- `request_next_activation(sc_core::sc_event)`: Schedule execution immediately when a specific DE event fires.

## Complete End-to-End Example: Dynamic PWM Generator

Below is a complete, compilable example of a PWM (Pulse Width Modulator) that uses Dynamic TDF. Instead of running a fixed high-frequency TDF timestep (which is computationally expensive), the module dynamically calculates the exact time of the next rising or falling edge and yields control back to the scheduler.

```cpp
#include <systemc-ams>

// Dynamic TDF PWM Generator
SCA_TDF_MODULE(dynamic_pwm) {
    sca_tdf::sca_in<double>  duty_cycle; // Input: 0.0 to 1.0
    sca_tdf::sca_out<double> pwm_out;    // Output: 0.0 or 1.0

    double period;          // Total period of the PWM signal
    double current_duty;    // Latched duty cycle
    bool   is_high;         // Current output state

    SCA_CTOR(dynamic_pwm) : duty_cycle("duty_cycle"), pwm_out("pwm_out"),
                            period(10.0), current_duty(0.5), is_high(true) {}

    void set_attributes() {
        // 1. Opt-in to Dynamic TDF
        // Alerts the AMS kernel that this module's sca_node will shift time
        does_attribute_changes();

        // Ensure the input port also accepts the dynamic scheduling changes
        duty_cycle.set_timestep(1.0, sc_core::SC_MS);
    }

    // 2. The dynamic attributes callback (Executes before processing)
    void change_attributes() {
        // Calculate sleep time based on the state
        double time_to_next_edge = 0.0;

        if (is_high) {
            // We are high, wait for the duty cycle duration
            time_to_next_edge = period * current_duty;
        } else {
            // We are low, wait for the remainder of the period
            time_to_next_edge = period * (1.0 - current_duty);
        }

        // Clamp the time to avoid zero-delay loops on 0% or 100% duty cycles
        if (time_to_next_edge < 0.001) time_to_next_edge = 0.001;

        // 3. Request activation precisely at the next edge
        request_next_activation(sc_core::sc_time(time_to_next_edge, sc_core::SC_MS));
    }

    // 4. The processing callback runs exactly when requested
    void processing() {
        // If we are at the start of a period (transitioning to high), read new duty cycle
        if (!is_high) {
            current_duty = duty_cycle.read();
            // Clamp duty cycle for safety
            if (current_duty > 1.0) current_duty = 1.0;
            if (current_duty < 0.0) current_duty = 0.0;
        }

        // Toggle state
        is_high = !is_high;

        // Write output
        if (is_high) {
            pwm_out.write(1.0);
        } else {
            pwm_out.write(0.0);
        }
    }
};

// Simple stimulus generating a varying duty cycle
SCA_TDF_MODULE(duty_stimulus) {
    sca_tdf::sca_out<double> duty_out;

    SCA_CTOR(duty_stimulus) : duty_out("duty_out") {}

    void set_attributes() {
        // We must accept dynamic timing shifts from the PWM generator
        // This alerts the sca_sync_value_provider that this module is cluster-safe
        accept_attribute_changes();
        // Give a default static timestep, though it will be driven dynamically
        set_timestep(1.0, sc_core::SC_MS);
    }

    void processing() {
        // Slowly increase the duty cycle over time
        double time_ms = sc_core::sc_time_stamp().to_seconds() * 1000.0;
        double duty = 0.1 + (time_ms / 100.0) * 0.1;
        if (duty > 0.9) duty = 0.9;

        duty_out.write(duty);
    }
};

int sc_main(int argc, char* argv[]) {
    // Signals
    sca_tdf::sca_signal<double> sig_duty("sig_duty");
    sca_tdf::sca_signal<double> sig_pwm("sig_pwm");

    // Instantiation
    duty_stimulus stim("stim");
    stim.duty_out(sig_duty);

    dynamic_pwm pwm("pwm");
    pwm.duty_cycle(sig_duty);
    pwm.pwm_out(sig_pwm);
    pwm.period = 10.0; // 10 ms period

    // Tracing
    sca_util::sca_trace_file* tf = sca_util::sca_create_tabular_trace_file("pwm_trace");
    sca_util::sca_trace(tf, sig_duty, "duty_cycle");
    sca_util::sca_trace(tf, sig_pwm, "pwm_out");

    std::cout << "Starting Dynamic TDF Simulation..." << std::endl;
    sc_core::sc_start(100.0, sc_core::SC_MS); // Simulate for 100 ms

    sca_util::sca_close_tabular_trace_file(tf);

    std::cout << "Simulation finished. Inspect pwm_trace.dat." << std::endl;

    return 0;
}
```

### Why is this powerful?
In standard static TDF, to accurately model a PWM signal with 1% resolution on a 10ms period, you would be forced to run the `processing()` callback every `0.1 ms`, resulting in 1,000 executions over 100ms.
With Dynamic TDF, the module pushes a targeted `sc_event` to the kernel and sleeps entirely between edges, executing `processing()` only **twice** per period (20 executions total), regardless of the duty cycle resolution. This bypasses massive amounts of cluster evaluation overhead while maintaining perfect temporal accuracy.

## Lesson 55: AMS Attribute Negotiation and Cluster Scheduling

Canonical lesson: https://www.learn-systemc.com/tutorials/054-ams-attribute-negotiation-and-cluster-scheduling

How TDF attributes, rates, delays, timesteps, and converter ports shape the AMS schedule before simulation runs.

## How to Read This Lesson

AMS scheduling is easier if you stop imagining a free-running C++ loop. Think of each TDF module as declaring timing attributes. The AMS kernel uses those declarations to build a schedule that can synchronize with SystemC discrete-event time.

## Standard and source context

Use `Docs/LRMs/SystemC_AMS_2_0_LRM.pdf` for TDF modules, attributes, rates, delays, timesteps, converter ports, and synchronization behavior. Use the existing AMS lessons for TDF, dynamic TDF, LSF, ELN, and converter-port context.

## The TDF Attribute Lifecycle

A TDF module typically has three important callbacks:

```cpp
void set_attributes();
void initialize();
void processing();
```

Read them this way:

- `set_attributes()` declares timing and rate contracts.
- `initialize()` prepares state after the schedule is known.
- `processing()` performs the repeated sample-level computation.

The important point is that attributes are not casual configuration variables. They shape the static schedule.

The LRM describes this as an attribute propagation problem. A module may declare a timestep, rate, or delay locally, but the solver has to propagate that information through connected ports and neighboring modules until the whole cluster has one consistent schedule. If two declarations cannot be reconciled, the problem is caught before useful simulation begins, which is exactly what you want for analog timing bugs.

## Rates, Delays, and Timesteps

Rate tells the scheduler how many samples move per activation. Delay tells it how many samples of history exist on a port. Timestep tells it the time distance represented by samples.

Example shape:

```cpp
SCA_TDF_MODULE(Fir) {
  sca_tdf::sca_in<double> in;
  sca_tdf::sca_out<double> out;

  SCA_CTOR(Fir) {}

  void set_attributes() override {
    in.set_rate(1);
    out.set_rate(1);
    out.set_timestep(10.0, sc_core::SC_NS);
  }

  void processing() override {
    out.write(0.5 * in.read());
  }
};
```

The module does not call `wait(10, SC_NS)` inside `processing()`. The timestep is part of the AMS schedule.

## Converter Ports Are Synchronization Boundaries

Converter ports connect AMS timing to SystemC discrete-event timing. This is where many bugs hide.

When DE code writes a value that feeds AMS, the AMS side samples that value according to its own schedule and converter rules. When AMS writes back to DE, the result becomes visible through scheduled synchronization points.

The useful review question is: at exactly which simulation times can this value cross the boundary?

## Static vs Dynamic TDF

Static TDF builds a schedule from fixed attributes. Dynamic TDF lets a module influence its next activation time, which is powerful for reactive models such as PWM, event-driven analog behavior, and variable-rate systems.

Use dynamic TDF only when the model really needs it. Static schedules are easier to reason about and usually faster.

## Review Checklist

- Are rates and timesteps documented in engineering units?
- Does every delay have a modeling reason?
- Are converter-port boundary times understood?
- Is dynamic TDF used only for genuinely dynamic timing?
- Does the AMS detail answer the platform question, or is it slowing a model that only needed a DE approximation?

## Lesson 56: AMS API and Model-of-Computation Field Guide

Canonical lesson: https://www.learn-systemc.com/tutorials/133-ams-api-and-model-of-computation-field-guide

Individual SystemC AMS symbols for TDF, LSF, ELN, converter ports, AC analysis, and mixed-signal synchronization.

## How to Read This Lesson

AMS is easiest when you group names by model of computation. TDF names describe sampled-time data flow. LSF names describe linear signal-flow equations. ELN names describe electrical conservative networks. Converter names tell you where AMS time meets SystemC discrete-event time.

## Standard and source context

Use `Docs/LRMs/SystemC_AMS_2_0_LRM.pdf`. This repository currently has the AMS LRM locally, but no checked-out AMS source tree, so this guide focuses on standard API coverage and modeling meaning.

## TDF Timing and Scheduling

`sca_tdf`, `sca_tdf::sca_module`, `sca_tdf::sca_in`, `sca_tdf::sca_out`, `sca_tdf::sca_signal`, `sca_time`, `set_timestep`, `set_rate`, `set_delay`, `initialize`, `set_attributes`, and `processing` are the core TDF vocabulary.

`sca_module_method`, `sca_delay`, `sca_sampling`, `sca_multirate`, `sca_multirate_fmt`, `sca_parameter`, `sca_parameter_base`, `sca_port`, `sca_interface`, `sca_signal_if`, `sca_prim_channel`, `sca_out_base`, and `sca_node_if` are supporting API names around module callbacks, attributes, ports, and channel-like structure.

Read `set_attributes` as a contract declaration, not normal runtime code. The kernel uses rates, delays, and timesteps to build a static schedule. The schedule is then synchronized to SystemC time at converter boundaries.

`sca_default_interpolator`, `sca_decimation`, and related rate-conversion ideas appear when sampling rates differ. Use them carefully: rate conversion is a modeling decision, not just an API call to silence a scheduler error.

## Converter and DE Boundary Names

`sca_de_source`, `sca_de_sink`, `sca_de_mux`, `sca_de_demux`, `sca_de_gain`, `sca_de_c`, `sca_de_l`, `sca_de_r`, `sca_de_isource`, `sca_de_vsink`, `sca_de_vsource`, `sca_de_isink`, `sca_de_rswitch`, and converter-port concepts describe the bridge between discrete-event SystemC and AMS domains.

Shorter component-style names such as `sca_mux`, `sca_demux`, `sca_isink`, `sca_info`, `sca_rswitch`, `sca_vccs`, `sca_vcvs`, and `sca_transmission_line` appear in the same ecosystem. Read them by signal-flow role: source, sink, selector, diagnostic, switch, controlled source, or transmission line.

TDF primitive names such as `sca_tdf_in`, `sca_tdf_out`, `sca_tdf_signal`, `sca_tdf_source`, `sca_tdf_sink`, `sca_tdf_mux`, `sca_tdf_demux`, `sca_tdf_gain`, `sca_tdf_c`, `sca_tdf_l`, `sca_tdf_r`, `sca_tdf_isink`, `sca_tdf_isource`, `sca_tdf_vsink`, `sca_tdf_vsource`, `sca_tdf_rswitch`, `sca_tdf_sc_in`, `sca_tdf_sc_out`, `sca_tdf_out_ct_cut`, `sca_tdf_out_dt_cut`, and `sca_tdf_solver` tell you the exact boundary between sampled TDF behavior, DE converter ports, and analog-style primitive blocks.

The boundary question is always timing: when is a DE value sampled by AMS, and when does an AMS result become visible to DE? If that answer is vague, the model may be correct-looking but timing-ambiguous.

## LSF Names

LSF models mathematical signal-flow equations. Look for integrators, gains, adders, transfer functions, and source/sink components. `sca_lsf` names are best reviewed by drawing the equation graph beside the C++ instances.

`sca_lsf_in`, `sca_lsf_out`, `sca_lsf_signal`, `sca_lsf_solver`, `sca_ltf_zp`, `sca_ss`, and `sca_matrix` are LSF-specific or solver-adjacent names. They point to linear equations, transfer functions, state-space representation, and matrix solving rather than event scheduling.

If a block is really an algorithm with sample timing, TDF may be clearer. If it is a continuous-time linear relation, LSF often communicates the intent better.

## ELN Names

ELN models electrical networks. Names such as `sca_eln`, `sca_r`, `sca_c`, `sca_l`, `sca_vsource`, `sca_isource`, controlled sources like `sca_cccs` and `sca_ccvs`, and node/terminal concepts describe conservative electrical relationships.

`sca_eln_solver`, `sca_gyrator`, `sca_ideal_transformer`, `sca_nullor`, and related ELN primitives are physical-network modeling terms. If those names show up, draw the circuit graph before reading the C++ syntax.

Use ELN when current, voltage, and network topology matter. Do not force ELN into a fast VP just because it feels more physically accurate; the right abstraction is the one that answers the design question.

## AC and Noise Analysis Names

`sca_ac_f`, `sca_ac_delay`, `sca_ac_ltf_nd`, `sca_ac_ltf_zp`, `sca_ac_ss`, `sca_ac_scale`, `sca_ac_format`, `sca_ac_fmt`, `sca_ac_is_running`, and `sca_ac_noise_is_running` belong to small-signal AC and noise analysis support.

`sca_noise_format`, `sca_noise_fmt`, `sca_information_mask`, `sca_information_on`, `sca_information_off`, and `sca_max_time` are analysis, diagnostics, and limit-control vocabulary. They do not replace model equations; they control how analysis and reporting are configured.

The mental model is different from transient simulation. You are asking for frequency-domain behavior around a linearized system, not stepping through every time sample. Keep that distinction visible in lesson notes and code reviews.

## Proxy and Vector Utility Names

`sca_assign_from_proxy`, `sca_assign_to_proxy`, `sca_create_vector`, `sca_ct_proxy`, `sca_ct_vector_proxy`, and `sca_cut_policy` are utility-level names you may see while reading API implementation or generated documentation.

`sca_release`, `sca_is_prerelease`, `sca_copyright`, and `sca_copyright_string` are version/distribution names. They help diagnose library identity but do not change model semantics.

`sca_version`, `sca_version_major`, `sca_version_minor`, and `sca_version_originator` are the same kind of version metadata. `sca_traceable_object`, `sca_trace_variable`, and `sca_trace_mode_base` are tracing vocabulary for making AMS quantities visible during debug.

Treat these as support machinery. They are useful for understanding how the library presents natural C++ syntax, but most model authors should stay focused on modules, ports, attributes, and physical meaning.

## AMS Review Checklist

- Which model of computation is being used: TDF, LSF, ELN, or DE?
- Where are rates, timesteps, and delays declared?
- Are converter boundary times understood?
- Is AC/noise analysis being used for the right kind of question?
- Does the model include more analog detail than the platform actually needs?

## Lesson 57: Configuration, Control & Inspection (CCI)

Canonical lesson: https://www.learn-systemc.com/tutorials/055-configuration-control-and-inspection-cci

Mastering the IEEE 1666.1 CCI Standard for standardizing parameter configuration in complex SystemC SoCs.

## How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

# Configuration, Control & Inspection (CCI)

As SystemC models grow from simple IP blocks into massive, multi-core System-on-Chip (SoC) virtual platforms, configuring them becomes a monumental task.

Historically, designers used C++ constructor arguments, `#define` macros, or custom text-file parsers to configure things like memory sizes, clock speeds, or debug verbosity. This fragmented ecosystem meant IP from Company A couldn't be configured by the same tools as IP from Company B.

To solve this, Accellera standardizes configuration via the **IEEE 1666.1 Configuration, Control, and Inspection (CCI)** standard.

## Standard and source context

## The Problem CCI Solves & The Accellera Architecture

Imagine a virtual platform integrating an ARM processor and a custom memory controller. How does the user configure the memory size without modifying the underlying C++ IP source code?

CCI provides a unified API to:
1. Define configurable parameters within modules using `cci_param`.
2. Set initial values via a central Broker before elaboration.
3. Lock parameters to prevent accidental modification at runtime.
4. Track modifications via callbacks.

**Under the Hood (Accellera `cci` repository):**
The architecture centers around the `cci_broker_if` abstract interface and its default implementation, `cci_core_broker`.
When `cci_get_broker()` is called, it queries the `cci_broker_manager` singleton, which walks the `sc_core::sc_object` hierarchy tree to find the nearest broker. If none is explicitly set for a sub-hierarchy, the global fallback broker is used.

Furthermore, how does CCI store values of varying types dynamically? Through `cci_value`. In the source code, `cci_value` is essentially a type-safe JSON Abstract Syntax Tree (AST). It uses a union-like structure (or `std::variant` equivalents in newer standards) combined with string mappings to support booleans, numbers, strings, lists, and dictionaries. This allows external JSON configuration files to cleanly map into typed `cci_param<T>` instances.

## Complete CCI Setup Example

The following end-to-end example demonstrates how to set up a Global Broker, configure preset values, and instantiate an IP block that reads those parameters.

```cpp
#include <systemc>
#include <cci_configuration>

// A typical IP block using CCI parameters
SC_MODULE(MemoryController) {
    // cci_param wraps the standard types for broker integration
    cci::cci_param<int> mem_size;
    cci::cci_param<bool> debug_mode;

    SC_CTOR(MemoryController)
        // Parameters are named and provided with default values and descriptions
        : mem_size("mem_size", 1024, "Size of the memory in bytes")
        , debug_mode("debug_mode", false, "Enable debug tracing")
    {
        SC_METHOD(do_memory_op);

        // Example: Lock the parameter so it cannot be changed dynamically during simulation
        mem_size.lock();
    }

    void do_memory_op() {
        if (debug_mode.get_value()) {
            std::cout << "@" << sc_core::sc_time_stamp()
                      << " [MemoryController] Operating on memory of size: "
                      << mem_size.get_value() << " bytes" << std::endl;
        }
    }
};

// Top-level module encapsulating the IP
SC_MODULE(TopModule) {
    MemoryController mem_ctrl;

    SC_CTOR(TopModule) : mem_ctrl("mem_ctrl") {}
};

int sc_main(int argc, char* argv[]) {
    // 1. Get the global broker instance
    cci::cci_broker_handle broker = cci::cci_get_broker();

    // 2. Set parameters BEFORE the modules are instantiated.
    // The hierarchical name must match exactly: "top.mem_ctrl.mem_size"
    broker.set_initial_preset_value("top.mem_ctrl.mem_size", cci::cci_value(4096));
    broker.set_initial_preset_value("top.mem_ctrl.debug_mode", cci::cci_value(true));

    // 3. Instantiate the top module.
    // The MemoryController inside will now initialize its cci_params using the broker's preset values!
    TopModule top("top");

    std::cout << "Starting simulation with configured CCI parameters..." << std::endl;

    // Start simulation
    sc_core::sc_start(10, sc_core::SC_NS);

    return 0;
}
```

## Advanced LRM Details: Callbacks and Locking

### Locking Parameters
According to IEEE 1666.1, a parameter can be locked using `lock()`. Once locked, any subsequent attempt to call `set_value()` on that parameter will throw a CCI error. This is crucial for parameters that define the physical topology of the hardware (e.g., bus widths, memory sizes) which physically cannot change after elaboration. In the implementation, locking simply sets an internal boolean flag `m_is_locked` in the `cci_param_untyped` base class, which is strictly verified before invoking the write operation.

### Parameter Callbacks
CCI allows you to register callbacks for parameter lifecycle events (creation, destruction, value changes). This is heavily utilized by GUI tools to dynamically update property views. In the source code, `cci_param` maintains an internal vector of `cci_callback_untyped_handle`. When `set_value()` executes, it iterates over these handles, dynamically invoking the registered function pointers.

```cpp
#include <systemc>
#include <cci_configuration>

void on_debug_changed(const cci::cci_param_write_event<bool>& ev) {
    std::cout << ">>> CCI Event: debug_mode changed from "
              << ev.old_value << " to " << ev.new_value << std::endl;
}

SC_MODULE(CallbackDemo) {
    cci::cci_param<bool> debug_mode;

    SC_CTOR(CallbackDemo) : debug_mode("debug_mode", false) {
        // Registering a post-write callback
        debug_mode.register_write_callback(&on_debug_changed);

        SC_THREAD(modifier_thread);
    }

    void modifier_thread() {
        wait(10, sc_core::SC_NS);
        debug_mode.set_value(true); // Triggers the callback
    }
};

int sc_main(int argc, char* argv[]) {
    CallbackDemo demo("demo");
    sc_core::sc_start(20, sc_core::SC_NS);
    return 0;
}
```

By conforming to IEEE 1666.1, your models ensure interoperability across all standard SystemC virtual platforms, eliminating proprietary setup files.

## Lesson 58: CCI Bridge: Guidelines and Integration

Canonical lesson: https://www.learn-systemc.com/tutorials/056-cci-bridge-guidelines-and-integration

Practical CCI modeler guidelines for parameter ownership, descriptions, callbacks, custom value types, and platform integration.

## How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

# CCI Bridge: Guidelines and Integration

The IEEE 1666.1 SystemC CCI standard is highly flexible, but its effectiveness depends heavily on models following consistent engineering guidelines. When developing Virtual Platforms or intellectual property (IP) blocks using CCI, adhering to standardized practices ensures that your models can be easily integrated, queried, and managed by top-level configuration tools.

## Standard and source context

## 1. Keep Parameters Owned by the Hierarchy

Parameters should not be floating in global scope. They must be declared as part of the `sc_core::sc_module` that owns the corresponding behavior. This ensures that parameter hierarchical names (e.g., `top.memory.size`) logically match the model structure.

Avoid creating a giant, monolithic "configuration struct" passed down from the top level, as this defeats the distributed, decentralized nature of CCI parameters.

## 2. Always Provide Descriptions

Descriptions are not optional in serious, industrial platforms. Tools can extract parameter descriptions via `cci_param::get_description()` to generate user-facing configuration GUIs, auto-generate markdown documentation, or display help in a command-line interface. Future maintainers need to know the semantic intent of a parameter.

## 3. Prefer Typed Access Internally

While CCI brokers deal heavily in variant types (`cci::cci_value`), IP models should use strongly typed access internally for safety and performance:

```cpp
// Preferred within the module:
uint64_t bytes = memory_size.get_value();
```

Untyped access (`set_cci_value()`, `get_cci_value()`) should be strictly reserved for generic tools, JSON parsers, and infrastructure components that do not know the schema at compile time.

## 4. Validate Early

Validate configuration values as early as possibleâ€”typically in `end_of_elaboration()` or `start_of_simulation()`â€”before the simulation behavior begins depending on them. Examples of critical validations include:
- Memory size must be a power of two.
- Timing latency must be strictly greater than zero.
- Address ranges must not overlap with another target.
- A boolean feature flag combination must be legally supported by the hardware model.

## 5. Be Conservative with Custom Value Types

CCI can support user-defined value types, but using them requires registering custom type traits and serialization functions. If a configuration can be expressed using a simple map, list, integer, string, or boolean, strongly prefer the common built-in types. This maximizes compatibility with external tools (like JSON bridges) that only understand standard primitives.

## Under the Hood: `cci_utils::consuming_broker` and Preset Tracking

When following the guidelines above, checking for typos via `get_unconsumed_preset_values()` is a critical step. In the Accellera `cci` repository, the `cci_utils::consuming_broker` implements this behavior by maintaining two primary `std::map` data structures:
1. `std::map<std::string, cci_value> m_presets`: Stores all preset values injected into the simulation before the modules are constructed.
2. `std::vector<std::string> m_consumed_presets`: Tracks which keys from `m_presets` were actually queried during the initialization of `cci_param` objects.

When a module constructs a `cci_param` (e.g., `top.mem.size_bytes`), the parameter asks the active `cci_broker_if` for its initial value. Inside `consuming_broker::get_preset_cci_value(const std::string& name)`, the broker searches `m_presets`. If found, the name is pushed into `m_consumed_presets`.

Later, when your integration layer calls `get_unconsumed_preset_values()`, the broker internally computes the set difference between the keys in `m_presets` and the elements in `m_consumed_presets`, returning a filtered map of typos, orphaned parameters, or version mismatches. This explicit tracking mechanism prevents silent configuration failures.

## Complete Integration Pattern Example

A platform-level configuration flow in a real Virtual Platform often looks like this:
1. Register the global broker.
2. Load configuration file or command-line values (simulated here via presets).
3. Construct the module hierarchy (parameters consume the presets).
4. Validate the parameter set (e.g., check for unconsumed presets or invalid values).
5. Start the simulation.

The following complete, end-to-end `sc_main` demonstrates this exact integration pattern:

```cpp
#include <systemc>
#include <cci_configuration>
#include <iostream>
#include <vector>

// 1. Parameter Ownership: Parameters belong to the module
class MemoryModel : public sc_core::sc_module {
public:
    // 2. Descriptions: Always provided
    cci::cci_param<uint64_t> size_bytes;
    cci::cci_param<uint32_t> latency_ns;

    SC_HAS_PROCESS(MemoryModel);
    MemoryModel(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , size_bytes("size_bytes", 1024 * 1024, "Memory size in bytes (must be power of two)")
        , latency_ns("latency_ns", 10, "Access latency in nanoseconds (must be > 0)")
    {
    }

    // 4. Validate Early: Check parameters before simulation runs
    void end_of_elaboration() override {
        uint64_t size = size_bytes.get_value(); // 3. Typed access
        uint32_t latency = latency_ns.get_value();

        if (latency == 0) {
            SC_REPORT_FATAL("MemoryModel", "Latency must be strictly greater than 0.");
        }

        // Simple power-of-two check
        if (size == 0 || (size & (size - 1)) != 0) {
            SC_REPORT_FATAL("MemoryModel", "Memory size must be a power of two.");
        }

        std::cout << name() << " initialized successfully with size "
                  << size << " bytes and latency " << latency << " ns.\n";
    }
};

class PlatformTop : public sc_core::sc_module {
public:
    MemoryModel mem;

    PlatformTop(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , mem("mem") // Instantiates "top.mem.size_bytes" etc.
    {}
};

int sc_main(int argc, char* argv[]) {
    // Step A: Register broker
    cci::cci_register_broker(new cci_utils::consuming_broker("Platform_Broker"));
    cci::cci_broker_handle broker = cci::cci_get_broker();

    // Step B: Load configuration (e.g., from command line or JSON)
    // We simulate an external tool setting presets here.
    broker.set_preset_cci_value("top.mem.size_bytes", cci::cci_value(2048));
    broker.set_preset_cci_value("top.mem.latency_ns", cci::cci_value(5));

    // We deliberately create a typo to demonstrate validation
    broker.set_preset_cci_value("top.mem.latancy_ns", cci::cci_value(20));

    // Step C: Construct modules
    PlatformTop top("top");

    // Step D: Validate parameter set at the platform level (check unconsumed presets)
    auto unconsumed = broker.get_unconsumed_preset_values();
    if (!unconsumed.empty()) {
        std::cout << "\n--- WARNING: Configuration Typos Detected ---\n";
        for (const auto& preset : unconsumed) {
            std::cout << "Unconsumed preset: " << preset.first << "\n";
        }
        std::cout << "---------------------------------------------\n\n";
    }

    // Step E: Start simulation (triggers end_of_elaboration validation internally)
    sc_core::sc_start();

    return 0;
}
```

Following these guidelines ensures that your models remain highly reusable, safely configurable, and easily integrated into larger SystemC Virtual Platforms leveraging the IEEE 1666.1 standard.

## Lesson 59: CCI Brokers and Architecture

Canonical lesson: https://www.learn-systemc.com/tutorials/057-cci-brokers-and-architecture

Deep dive into SystemC CCI Brokers, handling parameter registration, global vs local brokers, and preset values.

## How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

# CCI Brokers and Architecture

The SystemC Configuration, Control and Inspection (CCI) standard (IEEE 1666.1) introduces a powerful, standardized mechanism for configuring models. At the heart of this architecture lies the relationship between **Parameters** and **Brokers**.

If you think of a parameter as a typed variable bound to a hierarchical name, the broker is the centralized registry that manages its existence, access control, and initial configuration. It is responsible for bridging the gap between the internal SystemC module hierarchy and external tools or top-level configuration scripts.

## Standard and source context

## What is a Broker?

A broker (implementing `cci::cci_broker_if`) is an object that aggregates parameters. It provides container behaviors such as finding parameters by name, enumerating all known parameters, and managing preset values.

Rather than interacting with the raw broker interface, applications and models use a `cci::cci_broker_handle`. This handle acts as a proxy, safely routing requests to the underlying broker while injecting `cci::cci_originator` information that represents the caller.

When the lessons say configuration broker, they mean this CCI broker role: a discoverable service that owns parameter registration, preset lookup, name-based inspection, and tool-facing access policy. In a large virtual platform, treating the broker as a first-class architecture element is what keeps configuration from turning into scattered constructor arguments.

### Global vs Local Brokers

CCI defines a hierarchy of brokers that typically mirrors the `sc_core::sc_module` hierarchy. The LRM defines three primary layers of broker interaction:

1. **Global Broker**: The ultimate fallback. There is exactly one global broker in the simulation, and it sits at the top level, above any `sc_module` hierarchy. It **must** be registered before any parameters or local brokers are created. If you fail to register a global broker before instantiating a parameter, the CCI library will enforce a default behavior, but explicit registration is strongly recommended.
2. **Local Brokers**: Modules can define their own "local" brokers to hide or manage parameters private to a sub-assembly. By registering a broker to a specific module, all parameters instantiated within that module (and its children) will default to communicating with the local broker.
3. **Automatic Broker Resolution**: When a parameter is created inside an `sc_module`, it registers with the "automatic" broker. The CCI runtime finds this broker by walking up the `sc_core::sc_object` hierarchy until a local broker is found. If none is found, the global broker takes responsibility.

## C++ Source Code Implementation Details: `cci_broker_if` and `cci_originator`

Under the hood, every broker must implement the pure virtual interface `cci_broker_if`. This interface defines the core contract for parameter management:
* `add_param(cci_param_if*)` and `remove_param(cci_param_if*)`: The runtime hooks where parameters inject themselves into the broker's maps upon construction and destruction.
* `get_param_handle(name)`: Returns a type-erased `cci_param_handle` referencing the underlying `cci_param_if`.
* `get_preset_cci_value(name)`: Retrieves the `cci_value` JSON-like AST payload.

When using a `cci_broker_handle`, the handle secretly injects a `cci_originator` into every call routed to the `cci_broker_if`. The originator is a critical security and traceability mechanism in CCI.
In the `accellera-official/cci` repository, when a `cci_originator` is constructed without arguments, it inspects the active simulation context via `sc_core::sc_get_current_process_handle()`. If a process is currently executing, the originator captures the hierarchical name of the process. If it is constructed during elaboration (e.g., in a module constructor), it captures the module's name. The broker uses this originator to determine if a specific entity has the permissions to lock or mutate a parameter.

## Initializing Parameters with Presets

One of the most important jobs of a broker is to manage **preset values**. A preset allows an external tool, testbench, or top-level script to define the value of a parameter *before the parameter even exists*.

When a parameter is finally constructed by its owning module, it queries its broker. If the broker has a preset value for that parameter's hierarchical name, the parameter adopts the preset value instead of its hardcoded constructor default.

### Unconsumed Presets

A common configuration error is a typo in the hierarchical name of a preset value. Because presets are stored in the broker until claimed by a newly constructed parameter, a preset with a typo will simply sit in the broker, unconsumed, while the target parameter silently falls back to its default value.

Brokers provide a way to detect this via `get_unconsumed_preset_values()`. This is typically checked at the `end_of_elaboration()` or `start_of_simulation()` phase to catch typos before runtime.

Tools may also encounter `cci_untyped_param` and untyped handles when they inspect a model without knowing every C++ template type in advance. Typed parameters are what model authors normally write, but untyped access is what lets generic tooling enumerate, display, serialize, or validate parameters across a mixed platform.

## Complete Example: Brokers and Presets

The following complete, compilable example demonstrates how to set up the global broker, apply preset values, instantiate a module with parameters, and check for unconsumed presets (e.g., due to typos).

```cpp
#include <systemc>
#include <cci_configuration>
#include <iostream>

// A simple module that defines CCI parameters
class MySubsystem : public sc_core::sc_module {
public:
    // CCI Parameters
    cci::cci_param<int> buffer_size;
    cci::cci_param<bool> enable_logging;

    SC_HAS_PROCESS(MySubsystem);
    MySubsystem(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        // Initialize parameters with default values
        , buffer_size("buffer_size", 256, "Size of the internal buffer")
        , enable_logging("enable_logging", false, "Enable verbose logging")
    {
        SC_METHOD(print_config);
    }

    void print_config() {
        std::cout << "[MySubsystem] " << name() << " Configuration:\n"
                  << "  buffer_size = " << buffer_size.get_value() << "\n"
                  << "  enable_logging = " << (enable_logging.get_value() ? "true" : "false")
                  << std::endl;
    }
};

int sc_main(int argc, char* argv[]) {
    // 1. Register the global broker before any modules or parameters are created.
    // cci_utils::consuming_broker is the standard implementation provided by the library.
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));

    // 2. Obtain a handle to the global broker
    cci::cci_broker_handle global_broker = cci::cci_get_broker();

    // 3. Set preset values for parameters that do not exist yet.
    // The hierarchical path includes the module instance name ("subsystem").
    global_broker.set_preset_cci_value("subsystem.buffer_size", cci::cci_value(1024));

    // We intentionally introduce a typo here to demonstrate unconsumed presets:
    global_broker.set_preset_cci_value("subsystem.enable_loging", cci::cci_value(true)); // Typo!

    // 4. Instantiate the module.
    // It will consume the "subsystem.buffer_size" preset automatically.
    MySubsystem subsystem("subsystem");

    // 5. Check for unconsumed presets before starting simulation.
    // This is a crucial step for catching typos in configuration files or scripts.
    std::vector<std::pair<std::string, cci::cci_value>> unconsumed =
        global_broker.get_unconsumed_preset_values();

    for (const auto& preset : unconsumed) {
        SC_REPORT_WARNING("CCI",
            ("Unconsumed preset value detected for parameter path: " + preset.first).c_str());
    }

    // 6. Start simulation
    sc_core::sc_start(1, sc_core::SC_NS);

    return 0;
}
```

### Explanation of the Flow

1. **`cci_register_broker`**: Installs a `consuming_broker` as the global fallback.
2. **`set_preset_cci_value`**: The broker records that if a parameter named `"subsystem.buffer_size"` is ever created, it should take the value `1024` instead of its hardcoded default.
3. **Instantiation**: `MySubsystem` is instantiated. When `buffer_size` is constructed, it queries the broker, finds the preset, and sets its initial value to `1024`.
4. **Typo Detection**: The preset `"subsystem.enable_loging"` contains a typo. The parameter `enable_logging` does not match, so it uses its default (`false`). The typo is caught by calling `get_unconsumed_preset_values()` and explicitly issuing an `SC_REPORT_WARNING`.

In the next tutorial, we will look closer at the `cci_param` class itself and how parameters manage mutability and originators.

## Lesson 60: CCI Parameters and Mutability

Canonical lesson: https://www.learn-systemc.com/tutorials/058-cci-parameters-and-mutability

Understanding cci_param, default values, mutability locks, and tracking parameter originators.

## How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

# CCI Parameters and Mutability

In the CCI standard, a parameter is a named instance of a specific compile-time type that registers itself with a broker. Parameters are the primary interface for module configuration, effectively replacing traditional C++ constructor arguments, struct-based config objects, or macro definitions.

The highest-level interface for a parameter is `cci::cci_param_if`, but in practice, you will instantiate concrete parameters using `cci::cci_param_typed<T>` (or its convenient alias `cci::cci_param<T>`).

## Standard and source context

## Parameter Instantiation

A parameter requires a name and a default value. It can optionally take a description and a scoping rule (`cci::cci_name_type`). When instantiated, the parameter immediately queries its managing broker. If a **preset value** was registered in the broker for this parameter's exact hierarchical name, the parameter initializes itself with the preset value. Otherwise, it falls back to the **default value** provided in the constructor.

## Mutability and Locking

According to IEEE 1666.1, not all configuration parameters should be modifiable during simulation. For instance, hardware structure (like cache sizes or bus widths) is typically fixed at elaboration. Modifying these values dynamically during the simulation phase could lead to undefined behavior in your SystemC logic (e.g., trying to resize a dynamically allocated array after simulation has started).

CCI provides two primary mechanisms to protect parameters from illegal modification: **Constant Mutability** and **Runtime Locking**.

### 1. Constant Mutability (`CCI_IMMUTABLE_PARAM`)

You can declare a parameter as immutable at compile-time by passing `cci::CCI_IMMUTABLE_PARAM` as a template argument. An immutable parameter takes its initial value (either from a preset or its default) and permanently rejects any subsequent modification attempts.

Any call to `set_value()` on an immutable parameter after its initial construction will result in an exception (specifically, a `cci_set_param_failure` error or similar diagnostic depending on the broker).

### 2. Runtime Locking

Alternatively, you can leave the parameter mutable but dynamically lock it at runtime using a password pointer. This is ideal for read-only status parameters published by a module: the module holds the "key" to unlock and update the parameter, while external observers can only read it.

If an external tool tries to `set_value()` on a locked parameter without providing the exact password pointer used to lock it, the write is rejected.

## Under the Hood: Mutability Traits and Pointer Locking

In the Accellera `cci` reference implementation, the mutability of a parameter is controlled by policy classes. When you instantiate a `cci_param<T>` (which defaults to `cci_param_typed<T, CCI_MUTABLE_PARAM>`), it inherits from `cci_param_untyped`.

If you explicitly use `CCI_IMMUTABLE_PARAM`, the underlying implementation class overrides the `set_value()` virtual methods to unconditionally throw an `SC_REPORT_ERROR` with a message indicating the parameter is strictly immutable. This check is resolved entirely by the C++ type system, ensuring that immutable parameters cannot be bypassed.

For runtime locking on mutable parameters, the `cci_param_if` implementation maintains an internal `void* m_lock_pwd` variable. When `lock(void* pwd)` is called, the parameter stores the raw memory address of `pwd`. Any subsequent call to `set_value(const T& val, void* pwd)` compares the provided `pwd` address against `m_lock_pwd`. If they do not match, the update is rejected. This provides a fast, zero-overhead access control mechanism natively embedded in the C++ memory model.

## Value Origin Tracking (`cci_originator`)

In large virtual platforms, it can be difficult to track *who* modified a parameter. Was it set by a top-level script? A backdoor debug tool? Or another SystemC module?

CCI solves this through the `cci_originator`. Whenever a parameter is created or its value is updated, an originator is recorded.
* If a parameter is updated from within the SystemC module hierarchy, the originator is automatically the `sc_core::sc_object` from which the call originated.
* If updated from an external tool, the originator is usually a string name (e.g., "Debug_GUI").

You can query the origin of a parameter's current value via `get_value_origin()`. You can also determine if the current value is the default or if it was overridden using `is_preset_value()` and `is_default_value()`.

## Complete Example: Mutability and Originators

Here is a complete, compilable example demonstrates immutable parameters, runtime locking, and originator tracking.

```cpp
#include <systemc>
#include <cci_configuration>
#include <iostream>

class CacheConfig : public sc_core::sc_module {
public:
    // 1. Immutable Parameter: Set at creation, cannot be changed later.
    cci::cci_param<int, cci::CCI_IMMUTABLE_PARAM> cache_size;

    // 2. Mutable Parameter with runtime locking: used for status reporting.
    cci::cci_param<std::string> status_msg;

    SC_HAS_PROCESS(CacheConfig);
    CacheConfig(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , cache_size("cache_size", 256, "Size of the cache in KB")
        , status_msg("status_msg", "Initializing", "Current state of the cache")
    {
        // Lock the status parameter using 'this' as the password pointer
        status_msg.lock(this);

        SC_THREAD(cache_behavior);
    }

    void cache_behavior() {
        wait(10, sc_core::SC_NS);

        // Update the status by unlocking, setting the value, and relocking
        status_msg.unlock(this);
        status_msg.set_value("Active");
        status_msg.lock(this);

        wait(10, sc_core::SC_NS);

        status_msg.unlock(this);
        status_msg.set_value("Idle");
        status_msg.lock(this);
    }
};

int sc_main(int argc, char* argv[]) {
    // Register the global broker
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
    cci::cci_broker_handle broker = cci::cci_get_broker();

    // Set a preset value for the immutable parameter before instantiation
    broker.set_preset_cci_value("cache.cache_size", cci::cci_value(1024));

    // Instantiate the module
    CacheConfig cache("cache");

    // Inspect parameter origin and mutability
    std::cout << "--- Pre-Simulation Inspection ---\n";

    // Check if cache_size took the preset or default
    if (cache.cache_size.is_preset_value()) {
        std::cout << cache.cache_size.name() << " is using a preset value: "
                  << cache.cache_size.get_value() << "\n";
    }

    // Attempting to modify an immutable parameter will throw an exception
    try {
        cache.cache_size.set_value(2048);
    } catch (const std::exception& e) {
        std::cout << "Expected Exception caught: Cannot modify immutable parameter.\n"
                  << "  -> " << e.what() << "\n";
    }

    // Attempting to modify a locked parameter with the wrong password will fail
    try {
        // We try to use nullptr as the password, but it was locked with 'this' (cache ptr)
        cache.status_msg.set_value("Hacked", nullptr);
    } catch (const std::exception& e) {
        std::cout << "Expected Exception caught: Cannot modify locked parameter without correct password.\n"
                  << "  -> " << e.what() << "\n";
    }

    std::cout << "\n--- Starting Simulation ---\n";

    // Trace the origin and value over time
    for (int i = 0; i < 3; ++i) {
        cci::cci_originator orig = cache.status_msg.get_value_origin();
        std::cout << "Time " << sc_core::sc_time_stamp() << ": "
                  << cache.status_msg.name() << " = " << cache.status_msg.get_value()
                  << " (Set by originator: " << orig.name() << ")\n";

        sc_core::sc_start(10, sc_core::SC_NS);
    }

    return 0;
}
```

### Understanding Metadata

Beyond core configuration values, parameters can also carry arbitrary metadata. This is a map of string keys to `cci::cci_value` types (a variant type capable of holding strings, ints, lists, etc.) that provide additional context for UI tools, such as acceptable ranges, display units, or documentation links.

```cpp
// Example of attaching metadata to a parameter
cache_size.add_metadata("unit", cci::cci_value("KB"));
cache_size.add_metadata("max_limit", cci::cci_value(8192));
```

In the next tutorial, we will explore the `cci_value` class in depth to understand how CCI handles variant, dynamically-typed configuration values.

## Lesson 61: CCI Variant Types (cci_value)

Canonical lesson: https://www.learn-systemc.com/tutorials/059-cci-variant-types-cci-value

Exploring the cci_value class for dynamically-typed configuration, JSON serialization, and user-defined converters.

## How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

# CCI Variant Types (cci_value)

To create a truly interoperable configuration API, SystemC CCI needs to pass configuration values between arbitrary models, external tools, and text-based parsers without knowing the specific C++ types at compile-time.

To achieve this, the IEEE 1666.1 standard relies heavily on the `cci::cci_value` classâ€”a variant type capable of holding arbitrarily complex, dynamically typed configuration data.

## Standard and source context

## The `cci_value_category`

A `cci_value` object always has a category (`cci::cci_value_category`) defining the type of data it currently holds. The base categories act as building blocks:

* `cci::CCI_NULL_VALUE` - Uninitialized or explicitly null.
* `cci::CCI_BOOL_VALUE` - Boolean.
* `cci::CCI_INTEGRAL_VALUE` - Integers (up to 64-bit).
* `cci::CCI_REAL_VALUE` - Floating-point numbers.
* `cci::CCI_STRING_VALUE` - Text strings.
* `cci::CCI_LIST_VALUE` - A heterogeneous array of `cci_value` objects.
* `cci::CCI_DICT_VALUE` (Map) - A dictionary of string keys mapping to `cci_value` objects.

## User-Defined Types

CCI cleanly handles basic C++ types. But what if you have a custom struct representing a coordinate, a MAC address, or an exact packet header that you want to configure as a single parameter?

To support this, you must define a template specialization of `cci::cci_value_converter<T>`. This converter explicitly tells the CCI library how to `pack` your C++ struct into a variant `cci_value` (usually a map/dictionary) and how to `unpack` it back out. Once defined, your custom type can be serialized to JSON and managed by brokers exactly like native integers or strings.

## Under the Hood: The `cci_value` AST and JSON Engine

In the Accellera reference implementation, `cci_value` is effectively an Abstract Syntax Tree (AST) node for configuration data. Internally, a `cci_value` object contains a tagged union (or pointer-to-implementation structure) that stores the actual payload alongside its `cci_value_category` enum. For primitive types like integers and booleans, the value is stored inline. For complex types like lists and maps, it stores pointers to heap-allocated `std::vector<cci_value>` or `std::map<std::string, cci_value>`.

When you call `cci_value::to_json()` or `cci_value::from_json()`, the Accellera reference implementation historically embeds a lightweight, high-performance JSON library (like RapidJSON) under the hood. The `to_json` method recursively traverses the `cci_value` AST, stringifying the tags according to strict JSON syntax.

The `cci_value_converter<T>` works via compile-time template instantiation. When you call `param.set_value(my_struct)`, the C++ compiler resolves to your explicit `pack()` template specialization, actively marshalling the struct's fields into the dynamically allocated `cci_value` dictionary AST before passing it to the broker. This isolates the expensive string manipulation/JSON operations exclusively to external tooling, while internal parameter accesses compile down to native C++ assignments.

## Complete Example: Values, JSON, and Converters

The following end-to-end example demonstrates how to:
1. Define a custom configuration struct.
2. Provide a `cci_value_converter` to pack and unpack it.
3. Instantiate a CCI parameter using this custom type.
4. Interact with `cci_value` dictionaries and JSON serialization.

```cpp
#include <systemc>
#include <cci_configuration>
#include <iostream>
#include <string>

// 1. A custom user-defined type
struct NetworkConfig {
    std::string ip_address;
    int port;
};

// 2. Specialize the converter in the cci namespace
namespace cci {
    template<>
    struct cci_value_converter<NetworkConfig> {
        typedef NetworkConfig type;

        // Pack the struct into a cci_value map
        static bool pack(cci_value::reference dst, const type& src) {
            dst.set_map()
               .push_entry("ip", cci_value(src.ip_address))
               .push_entry("port", cci_value(src.port));
            return true;
        }

        // Unpack a cci_value map back into the struct
        static bool unpack(type& dst, cci_value::const_reference src) {
            if (!src.is_map()) return false;

            cci_value::const_map_reference m = src.get_map();
            if (!m.has_entry("ip") || !m.has_entry("port")) return false;

            // Note: CCI does NOT implicitly convert strings to ints.
            // We must explicitly extract the right types.
            if (!m.at("ip").is_string() || !m.at("port").is_int()) return false;

            dst.ip_address = m.at("ip").get_string();
            dst.port = m.at("port").get_int();
            return true;
        }
    };
}

// 3. A module using the custom parameter
class Router : public sc_core::sc_module {
public:
    cci::cci_param<NetworkConfig> net_config;

    SC_HAS_PROCESS(Router);
    Router(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , net_config("net_config", {"192.168.1.1", 8080})
    {
        SC_METHOD(print_status);
    }

    void print_status() {
        NetworkConfig cfg = net_config.get_value();
        std::cout << "[Router] Started on IP: " << cfg.ip_address
                  << " Port: " << cfg.port << "\n";
    }
};

int sc_main(int argc, char* argv[]) {
    // Register the global broker
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
    cci::cci_broker_handle broker = cci::cci_get_broker();

    // 4. Working with cci_value natively
    // We can manually construct a cci_value dictionary (map) to use as a preset
    cci::cci_value preset_val;
    cci::cci_value::map_reference vmap = preset_val.set_map();
    vmap.push_entry("ip", cci::cci_value("10.0.0.1"));
    vmap.push_entry("port", cci::cci_value(9000));

    // Set the preset
    broker.set_preset_cci_value("router.net_config", preset_val);

    // Instantiate module
    Router router("router");

    // 5. JSON Serialization
    // Because we provided the converter, the CCI parameter can automatically
    // export its custom type as a JSON string!
    cci::cci_value current_val = router.net_config.get_cci_value();
    std::string json_str = current_val.to_json();

    std::cout << "--- JSON Serialization ---\n";
    std::cout << "Router Configuration as JSON: " << json_str << "\n\n";

    // 6. JSON Deserialization
    // We can also parse JSON directly back into a cci_value, and apply it.
    std::string new_json = "{\"ip\": \"127.0.0.1\", \"port\": 443}";
    cci::cci_value parsed_val = cci::cci_value::from_json(new_json);

    std::cout << "--- Applying new config via JSON ---\n";
    router.net_config.set_cci_value(parsed_val);

    // Start simulation to see the updated output
    sc_core::sc_start(1, sc_core::SC_NS);

    return 0;
}
```

### Key Takeaways from the LRM

* **Strict Type Checking**: If you call `.get_int()` on a `cci_value` holding a string (e.g., `"123"`), the system will throw an exception. The standard explicitly prohibits implicit type coercion. Always check with `.is_int()`, `.is_string()`, etc., before extracting.
* **Heterogeneous Lists**: A `cci_value_list` acts conceptually like an `std::vector<cci_value>`. A single list can legally contain an integer in index 0, a string in index 1, and a nested map in index 2.
* **Seamless Tooling**: By defining a `cci_value_converter`, custom C++ objects instantly gain JSON serialization capabilities and can be interacted with via external tools completely natively.

## Lesson 62: CCI Parameter Callbacks

Canonical lesson: https://www.learn-systemc.com/tutorials/060-cci-parameter-callbacks

How to use CCI parameter callbacks to monitor and validate configuration changes dynamically.

## How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

# CCI Parameter Callbacks

Configuration parameters in a SystemC SoC model rarely exist in isolation. Changing a base address parameter might require the model to re-map its memory sockets. Changing a clock speed might require re-calculating internal delay quantums.

To support dynamic behavior driven by configuration changes, the IEEE 1666.1 CCI API provides a comprehensive **Callback** system.

## Standard and source context

## The Four Callback Phases

Callables can be registered against four distinct stages of parameter access:

1. `register_pre_read_callback`: Invoked *before* the parameter's value is actually read. This can be used to lazily evaluate or update a parameter right before an external tool inspects it.
2. `register_post_read_callback`: Invoked *after* the value is read, just before it is returned to the caller.
3. `register_pre_write_callback`: Invoked *before* the new value is written to the parameter. This callback acts as a **validator**. If the callback returns `false`, the write is rejected and a `cci_set_param_failure` exception is thrown.
4. `register_post_write_callback`: Invoked *after* the parameter successfully updates its value. Used for side-effects (e.g., updating internal state).

## Typed vs Untyped Callbacks

If you know the underlying data type of the parameter, you can register a **typed callback**. The callback function receives an event object (e.g., `cci::cci_param_write_event<T>`) containing typed references to the values.

If you are writing a generic tool (like a logger or GUI) that does not know the parameter's type, you can register an **untyped callback**. It receives `cci::cci_param_write_event<void>`, and provides the old and new values as `cci::cci_value` variants.

## Under the Hood: Callback Execution Vectors

In the Accellera implementation, callbacks are structurally maintained as `std::vector` lists of functors (often backed by `std::function` or SFINAE-bound member function pointers) inside the `cci_param_impl` base class.

When `set_value()` is called, the parameter executes the following sequence:
1. It iterates through the `m_pre_write_callbacks` vector.
2. If **any** pre-write callback returns `false`, the iteration immediately halts. The parameter aborts the update, leaves the underlying `m_value` unchanged, and invokes `SC_REPORT_ERROR` (which usually throws a C++ exception unless configured otherwise).
3. If all pre-write callbacks return `true`, the parameter executes `m_value = new_value`.
4. Finally, it iterates through the `m_post_write_callbacks` vector, passing the event payload.

Because `set_value()` blocks until all callbacks return, you must avoid calling `wait()` inside a callback. Callbacks execute sequentially in the context of the thread that initiated the `set_value()`.

## Complete Example: Validation and Side-Effects

The following complete, compilable example demonstrates how a Timer peripheral uses a pre-write callback to reject invalid clock frequencies, and a post-write callback to recalculate its internal delay tick rate. It also shows a global untyped callback used for generic logging.

```cpp
#include <systemc>
#include <cci_configuration>
#include <iostream>

// A generic untyped logger for any parameter modification
void global_logger_callback(const cci::cci_param_write_event<void>& ev) {
    std::cout << "[Logger] Parameter '" << ev.param_handle.name()
              << "' changed from " << ev.old_value.to_json()
              << " to " << ev.new_value.to_json()
              << " (Originator: " << ev.originator.name() << ")\n";
}

class TimerPeripheral : public sc_core::sc_module {
public:
    cci::cci_param<int> frequency_hz;
    sc_core::sc_time tick_period;

    SC_HAS_PROCESS(TimerPeripheral);
    TimerPeripheral(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , frequency_hz("frequency_hz", 1000)
    {
        // 1. Register Typed Pre-Write Callback (Validator)
        // Returns true if valid, false to reject the write.
        frequency_hz.register_pre_write_callback(
            &TimerPeripheral::validate_frequency, this
        );

        // 2. Register Typed Post-Write Callback (Side-effects)
        // Using a C++11 Lambda for brevity.
        frequency_hz.register_post_write_callback(
            [this](const cci::cci_param_write_event<int>& ev) {
                this->recalculate_period(ev.new_value);
            }
        );

        // Initial calculation
        recalculate_period(frequency_hz.get_value());

        SC_THREAD(timer_thread);
    }

private:
    bool validate_frequency(const cci::cci_param_write_event<int>& ev) {
        if (ev.new_value <= 0) {
            std::cerr << "[Timer] Error: Frequency must be > 0. Rejected value: "
                      << ev.new_value << "\n";
            return false; // Reject
        }
        return true; // Accept
    }

    void recalculate_period(int freq) {
        tick_period = sc_core::sc_time(1.0 / freq, sc_core::SC_SEC);
        std::cout << "[Timer] Tick period updated to " << tick_period << "\n";
    }

    void timer_thread() {
        while(true) {
            wait(tick_period);
            // Timer tick logic would go here
        }
    }
};

int sc_main(int argc, char* argv[]) {
    // Register global broker
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
    cci::cci_broker_handle broker = cci::cci_get_broker();

    // Instantiate Module
    TimerPeripheral timer("timer");

    // 3. Register Untyped Callback dynamically
    // We request a handle to the parameter and register the generic logger
    cci::cci_param_untyped_handle untyped_h = broker.get_param_handle("timer.frequency_hz");
    if (untyped_h.is_valid()) {
        untyped_h.register_post_write_callback(&global_logger_callback);
    }

    // Start simulation
    sc_core::sc_start(1, sc_core::SC_MS); // Advance 1ms

    std::cout << "\n--- Triggering Valid Change ---\n";
    // This will trigger both the post-write lambda and the global logger
    timer.frequency_hz.set_value(2000);

    sc_core::sc_start(1, sc_core::SC_MS); // Advance another 1ms

    std::cout << "\n--- Triggering Invalid Change ---\n";
    // This will be rejected by the pre-write callback, throwing an exception
    try {
        timer.frequency_hz.set_value(-500);
    } catch (const std::exception& e) {
        std::cout << "Caught CCI Exception: " << e.what() << "\n";
    }

    return 0;
}
```

### Callback Lifecycle and Memory Management

When you register a callback, it returns a handle (e.g., `cci::cci_callback_untyped_handle`). You can use this handle to explicitly unregister the callback later if you no longer wish to monitor the parameter.

However, the CCI standard guarantees safe destruction. If the underlying parameter is destroyed (for example, its owning module is deleted dynamically, or the simulation ends), all callbacks are safely and automatically invalidated. You do not need to manually unregister callbacks in module destructors.

In the next tutorial, we will take a deeper dive into **Parameter Handles** and how external tools can safely manipulate parameters they do not own.

## Lesson 63: CCI Parameter Handles

Canonical lesson: https://www.learn-systemc.com/tutorials/061-cci-parameter-handles

Safely accessing and manipulating CCI parameters from outside their owning modules using parameter handles.

## How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

# CCI Parameter Handles

In SystemC, modules encapsulate their internal state. According to standard C++ object-oriented practices, a `cci::cci_param` is typically declared as a `private` or `protected` member of an `sc_core::sc_module`.

If it is private, how does a top-level configuration script, a GUI tool, or a testbench modify its value? Passing bare pointers to parameters breaks encapsulation and introduces dangerous dangling-pointer risks if the module is destroyed dynamically.

The IEEE 1666.1 CCI API solves this using **Parameter Handles**.

## Standard and source context

## What is a Parameter Handle?

A parameter handle acts as a safe, managed proxy to the underlying parameter instance. It provides almost all the functionality of the parameter itself (reading, writing, querying metadata, registering callbacks), but its lifecycle is explicitly decoupled from the parameter object.

If the parameter is destroyed, all handles to it are immediately marked invalid. Attempting to use an invalid handle will trigger a safe CCI error instead of a fatal C++ segmentation fault.

## Getting a Handle from the Broker

To interact with a parameter you do not own, you query the broker using the parameter's string-based hierarchical name. The broker returns a `cci_param_untyped_handle`. If the parameter does not exist, the handle is simply returned in an invalid state.

### Typed vs Untyped Handles

* **Untyped Handles (`cci_param_untyped_handle`)**: Excellent for generic tools (JSON parsers, GUIs) that operate purely on `cci_value` variants.
* **Typed Handles (`cci_param_typed_handle<T>`)**: If your testbench *knows* the C++ data type of the parameter, you can ask for a typed handle. This bypasses variant conversions, provides compile-time safety, and allows for native C++ assignment.

## Under the Hood: Handles and RTTI Casting

In the Accellera CCI implementation, a parameter handle is essentially a smart-pointer wrapper around a raw `cci_param_if*`. When a parameter is destroyed, the central broker actively nullifies or invalidates its internal registry entries, allowing handles to securely report `is_valid() == false` rather than dereferencing dangling memory.

When you request a typed handle via `broker.get_param_handle<T>("path")`, the broker first looks up the untyped `cci_param_if*` by string name. To convert it into a typed handle, the C++ library performs a `dynamic_cast< cci_param_typed<T>* >(param_if)`.

If the user requests `<int>` but the underlying parameter was instantiated as `<bool>`, the `dynamic_cast` safely fails (returns `nullptr`), and the broker explicitly returns an invalid handle. This Run-Time Type Information (RTTI) boundary guarantees that memory corruption cannot occur from mismatched type interpretations across generic APIs.

## Complete Example: Modifying Private Parameters via Handles

The following complete example demonstrates a module with private parameters and a top-level configuration function that safely manipulates those parameters using both typed and untyped handles.

```cpp
#include <systemc>
#include <cci_configuration>
#include <iostream>
#include <vector>

// 1. IP Block with strictly private configuration
class CacheIP : public sc_core::sc_module {
private:
    cci::cci_param<int> cache_size;
    cci::cci_param<bool> write_through;

public:
    SC_HAS_PROCESS(CacheIP);
    CacheIP(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , cache_size("cache_size", 256, "Size in KB")
        , write_through("write_through", false, "Write policy")
    {}

    void print_state() {
        std::cout << "[CacheIP] " << name() << " State:\n"
                  << "  size = " << cache_size.get_value() << " KB\n"
                  << "  write_through = " << (write_through.get_value() ? "ON" : "OFF") << "\n";
    }
};

// 2. An external configuration function (simulating a testbench or Python script bridge)
void run_external_configuration() {
    std::cout << "\n--- External Configuration Phase ---\n";
    cci::cci_broker_handle broker = cci::cci_get_broker();

    // A. Request an UNTYPED handle (e.g. if we were a generic JSON tool)
    cci::cci_param_untyped_handle h_wt = broker.get_param_handle("top.l1_cache.write_through");

    if (h_wt.is_valid()) {
        // Must use cci_value for untyped writes
        h_wt.set_cci_value(cci::cci_value(true));
        std::cout << "Successfully updated 'write_through' via untyped handle.\n";
    } else {
        std::cout << "Parameter 'write_through' not found.\n";
    }

    // B. Request a TYPED handle (e.g. if we are a C++ testbench that knows the type)
    cci::cci_param_typed_handle<int> h_size = broker.get_param_handle<int>("top.l1_cache.cache_size");

    if (h_size.is_valid()) {
        // Native C++ assignment works directly on typed handles!
        h_size = 1024;
        std::cout << "Successfully updated 'cache_size' to " << h_size.get_value()
                  << " via typed handle.\n";
    } else {
        std::cout << "Parameter 'cache_size' missing or type mismatch.\n";
    }
}

// 3. Bulk Retrieval Example
void dump_all_parameters() {
    std::cout << "\n--- Global Parameter Dump ---\n";
    cci::cci_broker_handle broker = cci::cci_get_broker();

    // Retrieve ALL parameter handles in the system
    std::vector<cci::cci_param_untyped_handle> all_params = broker.get_param_handles();

    for (auto handle : all_params) {
        std::cout << "Param: " << handle.name()
                  << " = " << handle.get_cci_value().to_json() << "\n";
    }
}

int sc_main(int argc, char* argv[]) {
    // Register the broker
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));

    // Instantiate the module wrapper
    struct Top : public sc_core::sc_module {
        CacheIP l1_cache;
        Top(sc_core::sc_module_name n) : sc_core::sc_module(n), l1_cache("l1_cache") {}
    };
    Top top("top");

    // Print initial state
    std::cout << "Before Configuration:\n";
    top.l1_cache.print_state();

    // Manipulate private parameters using handles
    run_external_configuration();

    // Dump everything
    dump_all_parameters();

    // Print final state to prove it worked
    std::cout << "\nAfter Configuration:\n";
    top.l1_cache.print_state();

    return 0;
}
```

> [!WARNING]
> Generating a handle for every single parameter in a massive SoC via `get_param_handles()` can be computationally expensive and consume memory. If you only need to inspect a subset of parameters, you should use the predicate-filtering overloaded version of `get_param_handles()` provided by the LRM.

## Best Practices for the Modeler

To effectively utilize the CCI standard in your virtual platforms:

1. **Keep Parameters Private:** Declare `cci_param` instances as `private` or `protected` in your `sc_module`. Force external modification to legitimately go through the broker and handles.
2. **Prefer Typed Handles in C++:** In hardcoded testbenches, use `cci_param_typed_handle<T>` to avoid `cci_value` variant-conversion overhead and strictly catch type mismatches at compile time.
3. **Always Check Validity:** A typo in a hierarchical string means `get_param_handle("...")` will return an invalid handle. Always check `.is_valid()` before dereferencing or setting a value to prevent exceptions.

## Lesson 64: CCI Source Architecture and Broker Internals

Canonical lesson: https://www.learn-systemc.com/tutorials/062-cci-source-architecture-and-broker-internals

How the CCI reference implementation organizes brokers, handles, parameters, originators, and preset consumption.

## How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

# CCI Source Architecture and Broker Internals

The SystemC Configuration, Control and Inspection (CCI) standard (IEEE 1666.1) specifies a precise architecture. Parameters register with brokers, external tools use handles, values are safely marshaled through `cci_value`, and every access carries originator information.

While the standard defines the interface, understanding how these components interact conceptually under the hood is critical for building robust tools.

## Standard and source context

## The Broker Hierarchy

A broker implements `cci::cci_broker_if`. It is fundamentally a registry and policy enforcement point. It has the following responsibilities:
- Managing the lifecycle of parameters (adding, removing, finding).
- Storing unconsumed presets (configurations provided before a parameter exists).
- Providing untyped and typed handles to requesting tools.
- Managing parameter mutability and locks.

When a parameter is constructed, the CCI runtime searches the `sc_core::sc_object` hierarchy upwards from the parameter's parent module to find the nearest local broker. If none is found, the global fallback broker is used.

## Local Brokers and `cci_utils::broker`

The CCI utility library provides `cci_utils::broker`, which can be registered locally to a specific module in the hierarchy using `cci::cci_register_broker`. When a local broker is registered:
1. It intercepts parameter creations within its module scope.
2. It can selectively hide parameters from the global broker or expose them.
3. It resolves presets locally before deferring to the global broker.

## Under the Hood: Hierarchical Delegation in C++

In the Accellera source code, `cci_utils::broker` implements local isolation by maintaining an explicit pointer to its parent broker (`cci_broker_if* m_parent`). During the `cci_register_broker(local_broker)` call, the CCI infrastructure walks up the `sc_object` tree. If it finds an existing broker on an ancestor, it assigns it to `m_parent`; otherwise, it assigns the global broker.

When a parameter inside the subsystem asks for its initial value, the local broker first checks its internal `std::map<std::string, cci_value> m_presets`. If the preset is not found locally, the local broker executes a recursive lookup: `m_parent->get_preset_cci_value(name)`.

Similarly, when an external tool calls `get_param_handles()` on the global broker, the global broker only returns parameters registered in its own `m_params` map. Because `secret_key` registers exclusively with the local broker's `m_params` map and is intentionally withheld from the global broker's exposure list, SFINAE and C++ scoping naturally prevent external tools from ever seeing or modifying it.

## Complete Example: Broker Hierarchy and Local Brokers

The following end-to-end example demonstrates how to set up a Global Broker, register a Local Broker within a subsystem, and observe how presets and parameter handles resolve through the hierarchy.

```cpp
#include <systemc>
#include <cci_configuration>
#include <iostream>
#include <vector>

// 1. A Subsystem with its own local broker
class SecureSubsystem : public sc_core::sc_module {
public:
    // The local broker instance
    cci_utils::broker local_broker;

    // Parameters managed by the local broker
    cci::cci_param<int> secret_key;
    cci::cci_param<int> public_id;

    SC_HAS_PROCESS(SecureSubsystem);
    SecureSubsystem(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , local_broker("Subsystem_Broker")
        , secret_key("secret_key", 0, "A hidden parameter")
        , public_id("public_id", 1, "A visible parameter")
    {
        // Register this broker to manage all parameters in this module and below
        cci::cci_register_broker(local_broker);

        // Expose public_id to the parent broker, but keep secret_key strictly local
        local_broker.expose.insert("top.secure_sub.public_id");
    }

    void print_state() {
        std::cout << "[SecureSubsystem] secret_key = " << secret_key.get_value()
                  << ", public_id = " << public_id.get_value() << "\n";
    }
};

// 2. The Top Level Module
class Top : public sc_core::sc_module {
public:
    SecureSubsystem secure_sub;

    Top(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , secure_sub("secure_sub")
    {}
};

int sc_main(int argc, char* argv[]) {
    // 3. Register the Global Broker
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
    cci::cci_broker_handle global_broker = cci::cci_get_broker();

    // Set presets at the global level
    global_broker.set_preset_cci_value("top.secure_sub.public_id", cci::cci_value(999));

    // This preset will NOT reach secret_key unless we set it directly on the local broker,
    // or if the local broker was configured to pull unexposed presets from the parent.
    global_broker.set_preset_cci_value("top.secure_sub.secret_key", cci::cci_value(42));

    // Instantiate the hierarchy
    Top top("top");

    std::cout << "--- Initial State ---\n";
    top.secure_sub.print_state(); // secret_key remains 0, public_id becomes 999

    // 4. Inspecting via the Global Broker Handle
    std::cout << "\n--- Global Broker Parameter Handles ---\n";
    std::vector<cci::cci_param_untyped_handle> global_params = global_broker.get_param_handles();
    for (auto& h : global_params) {
        std::cout << "Global sees: " << h.name() << "\n";
    }
    // Note: The global broker will NOT list "top.secure_sub.secret_key" because
    // it was not explicitly exposed by the local broker.

    // 5. Inspecting Unconsumed Presets
    std::cout << "\n--- Unconsumed Global Presets ---\n";
    auto unconsumed = global_broker.get_unconsumed_preset_values();
    for (const auto& preset : unconsumed) {
        std::cout << "Unconsumed: " << preset.first << " (Value: " << preset.second.to_json() << ")\n";
    }
    // "top.secure_sub.secret_key" will be listed as unconsumed in the global broker.

    sc_core::sc_start();
    return 0;
}
```

### Architectural Takeaways (IEEE 1666.1)

1. **Isolation**: Local brokers allow a module to completely isolate its internal configuration space from external tools. The global broker literally cannot see `secret_key` in the example above.
2. **Preset Routing**: Presets set on a global broker will only be consumed by local parameters if the local broker explicitly asks the parent for missing presets or exposes the parameter name to the parent.
3. **Handle Instantiation**: When you request a handle from a broker, the broker returns a proxy object. This avoids passing raw `cci_param_if*` pointers, protecting the system from dangling references if a parameter is dynamically destroyed.

## Lesson 65: CCI Mutability, Locking, and Lifecycle

Canonical lesson: https://www.learn-systemc.com/tutorials/063-cci-mutability-locking-and-lifecycle

Elaboration-only parameters, simulation-time updates, locks, callbacks, destruction, and safe configuration windows.

## How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

# CCI Mutability, Locking, and Lifecycle

Not every parameter should be changeable at any time during simulation. The IEEE 1666.1 CCI standard gives you a specific vocabulary and API to enforce mutability rules, protecting model invariants from unsafe runtime modifications.

## Standard and source context

## Mutability Categories

A parameter can be intended for elaboration-time configuration (structural) or simulation-time control (dynamic).

**Structural Parameters** (Elaboration-Time only):
- RAM size, address widths, or array dimensions.
- If these change after the `sc_core::sc_start()` phase begins, memory reallocation might corrupt pointers, or TLM sockets might become invalid.

**Dynamic Parameters** (Simulation-Time):
- Trace enables, log verbosity levels.
- Throttling delays or error-injection flags.

## Enforcing Immutability and Locks

If you do not have a robust mechanism to handle a parameter changing dynamically, you must lock it.

1. **`CCI_IMMUTABLE_PARAM`**: Passed as a template argument during instantiation, making the parameter permanently unchangeable after its initial constructor/preset evaluation.
2. **`.lock(void* pwd)`**: Dynamically locks the parameter at runtime. The parameter cannot be modified unless the caller provides the exact same password pointer (`pwd`) to `.unlock(void* pwd)`.
3. **`.is_locked()`**: Allows tools to query if a parameter is currently accepting changes.

Call this out explicitly in code reviews as parameter locking. A parameter that controls structure, address layout, socket topology, or allocation size should usually be locked before meaningful simulation starts. If a parameter remains writable, the owner should be able to explain what callbacks, synchronization, and invariants make that runtime update safe.

## Complete Example: Lifecycle and Safe Configuration Windows

A robust Virtual Platform often implements a "Safe Configuration Window." During this window (typically `end_of_elaboration`), the top-level module verifies configurations and locks down all structural parameters.

```cpp
#include <systemc>
#include <cci_configuration>
#include <iostream>

class MemoryController : public sc_core::sc_module {
public:
    // Structural parameter: must be locked before simulation starts.
    cci::cci_param<int> memory_size;

    // Dynamic parameter: can be changed during simulation.
    cci::cci_param<bool> enable_debug;

    SC_HAS_PROCESS(MemoryController);
    MemoryController(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , memory_size("memory_size", 1024)
        , enable_debug("enable_debug", false)
    {
        SC_THREAD(run_controller);
    }

    void run_controller() {
        while(true) {
            wait(10, sc_core::SC_NS);
            if (enable_debug.get_value()) {
                std::cout << "[MemCtrl] Debug tick at " << sc_core::sc_time_stamp() << "\n";
            }
        }
    }
};

class PlatformTop : public sc_core::sc_module {
public:
    MemoryController mem_ctrl;

    PlatformTop(sc_core::sc_module_name name) : sc_core::sc_module(name), mem_ctrl("mem_ctrl") {}

    // Safe Configuration Window: Lock structural parameters before simulation
    void end_of_elaboration() override {
        std::cout << "--- End of Elaboration: Locking Structural Parameters ---\n";

        // We lock 'memory_size' using 'this' as the password pointer.
        mem_ctrl.memory_size.lock(this);

        if (mem_ctrl.memory_size.is_locked()) {
            std::cout << "Success: " << mem_ctrl.memory_size.name() << " is locked.\n";
        }
    }
};

int sc_main(int argc, char* argv[]) {
    // 1. Setup Broker
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
    cci::cci_broker_handle broker = cci::cci_get_broker();

    // 2. Set Presets
    broker.set_preset_cci_value("top.mem_ctrl.memory_size", cci::cci_value(2048));

    // 3. Instantiate Platform
    PlatformTop top("top");

    // 4. Start simulation (Triggers end_of_elaboration, locking memory_size)
    sc_core::sc_start(15, sc_core::SC_NS);

    std::cout << "\n--- Attempting Runtime Configuration ---\n";

    // 5. Modifying an unlocked dynamic parameter (Succeeds)
    std::cout << "Enabling debug...\n";
    top.mem_ctrl.enable_debug.set_value(true);

    // 6. Attempting to modify a locked structural parameter (Fails)
    try {
        std::cout << "Attempting to change memory_size...\n";
        // This will throw cci_set_param_failure because it is locked!
        top.mem_ctrl.memory_size.set_value(4096);
    } catch (const std::exception& e) {
        std::cout << "Caught Expected Exception: " << e.what() << "\n";
    }

    // 7. Modifying a locked parameter with the CORRECT password (Succeeds)
    // (Usually this is only done internally by the owner, but shown here for completeness)
    top.mem_ctrl.memory_size.unlock(&top);
    top.mem_ctrl.memory_size.set_value(4096);
    top.mem_ctrl.memory_size.lock(&top);
    std::cout << "Successfully updated memory_size using correct password.\n\n";

    sc_core::sc_start(20, sc_core::SC_NS);

    return 0;
}
```

### Destruction Lifecycle

What happens to a parameter when its owning `sc_module` is dynamically destroyed?
The CCI standard dictates that when a `cci_param` is destroyed:
1. It unregisters itself from its managing broker.
2. It immediately invalidates all outstanding `cci_param_handle` proxies.
3. It safely invalidates all registered callbacks.

This lifecycle management guarantees that a long-running external tool (like a GUI) will not crash via segmentation fault if it attempts to query a handle for a hardware block that was hot-unplugged and deleted from the simulation.

## Under the Hood: Safe Handle Invalidation

In C++, ensuring a proxy object does not dereference a dangling pointer after the target is destroyed is a classic problem, typically solved using `std::weak_ptr` and `std::shared_ptr`. However, because CCI mandates strict performance and binary compatibility, the Accellera reference implementation achieves this manually.

When a `cci_param` executes its destructor, it calls `m_broker->remove_param(this)`. The broker actively locates the parameter in its internal `m_params` registry and erases it. More importantly, the internal `cci_param_impl` object maintains a list of all active `cci_param_handle` proxies that currently point to it. During destruction, the parameter iterates through this list, explicitly reaching into each handle and setting its internal `m_param_impl` pointer to `nullptr`.

Because a handle always checks `if (m_param_impl != nullptr)` inside `is_valid()` before forwarding method calls, this active-invalidation architecture guarantees that an external GUI trying to access a hot-unplugged parameter will gracefully receive an invalid response, rather than triggering a fatal C++ segmentation fault.

## Lesson 66: CCI Callbacks, Originators, and Tools

Canonical lesson: https://www.learn-systemc.com/tutorials/064-cci-callbacks-originators-and-tools

Pre-write and post-write callbacks, originator tracking, validation, introspection tools, and safe parameter observers.

## How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

# CCI Callbacks, Originators, and Tools

Callbacks allow a SystemC model to react when a CCI parameter changes. **Originators** tell the model *who* requested the access.

Together, they elevate CCI from a simple global variable table into a robust API capable of supporting complex Electronic Design Automation (EDA) tool flows, secure auditing, and trace generation.

## Standard and source context

## The Role of Originators

The IEEE 1666.1 standard requires every parameter modification to carry a `cci::cci_originator`.
- If a parameter is changed directly by a module in the SystemC hierarchy, the originator is automatically determined to be the current `sc_core::sc_object`.
- If an external tool (e.g., a Python script, a GUI, or a CLI parser) modifies a parameter via a handle, the tool can explicitly create a named originator (e.g., `cci_originator("Debug_GUI")`) and pass it to the setter function.

This allows models to log exactly who changed a configuration, or even reject writes from unprivileged originators in secure simulations.

## Under the Hood: The `cci_originator` Constructor

In the C++ reference implementation, when you omit the `cci_originator` argument in `set_value()` or `get_value()`, the library dynamically constructs a default originator on the stack.

Inside the default constructor of `cci_originator`, it probes the SystemC kernel directly:
1. It queries `sc_core::sc_get_current_process_handle()`. If the caller is running inside an `SC_THREAD` or `SC_METHOD`, it captures the exact hierarchical name of the active process.
2. If the simulation is not running (e.g., during elaboration or `sc_main`), it falls back to inspecting the current active module scope via `sc_core::sc_get_curr_simcontext()->hierarchy_curr()`.
3. It stores a constant reference to this string or object pointer internally.

Because it is constructed extremely frequently (on every parameter access), `cci_originator` is optimized as a lightweight proxy class. It avoids `std::string` heap allocations internally, storing only pointers to existing string literals or `sc_object` instances already maintained by the SystemC kernel.

## Complete Example: Tool Tracking via Originators and Callbacks

The following complete, compilable example demonstrates how to create custom originators, pass them through parameter handles, and use an untyped callback to generate an audit log of all configuration changes in the system.

```cpp
#include <systemc>
#include <cci_configuration>
#include <iostream>

// 1. A global audit logger using an untyped callback
void audit_log_callback(const cci::cci_param_write_event<void>& ev) {
    std::cout << "[AUDIT] Parameter '" << ev.param_handle.name()
              << "' changed:\n"
              << "        Old Value : " << ev.old_value.to_json() << "\n"
              << "        New Value : " << ev.new_value.to_json() << "\n"
              << "        Changed by: " << ev.originator.name() << "\n";
}

class NetworkInterface : public sc_core::sc_module {
public:
    cci::cci_param<std::string> mac_address;

    SC_HAS_PROCESS(NetworkInterface);
    NetworkInterface(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , mac_address("mac_address", "00:00:00:00:00:00", "Device MAC")
    {}

    // A method simulating internal hardware behavior changing the parameter
    void reset_mac_hardware() {
        std::cout << "\n--- Triggering internal hardware reset ---\n";
        // When set_value is called internally, the originator defaults to this sc_module.
        mac_address.set_value("FF:FF:FF:FF:FF:FF");
    }
};

int sc_main(int argc, char* argv[]) {
    // Register broker
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
    cci::cci_broker_handle broker = cci::cci_get_broker();

    // Instantiate module
    NetworkInterface eth0("eth0");

    // 2. Attach the audit logger to the parameter
    cci::cci_param_untyped_handle h_mac = broker.get_param_handle("eth0.mac_address");
    h_mac.register_post_write_callback(&audit_log_callback);

    // 3. Simulating an external CLI tool changing the value
    std::cout << "\n--- Simulating External CLI Tool ---\n";
    {
        // Explicitly create an originator representing the tool
        cci::cci_originator cli_originator("Command_Line_Parser");

        // Pass the originator into the handle's set function
        h_mac.set_cci_value(cci::cci_value("0A:1B:2C:3D:4E:5F"), cli_originator);
    }

    // 4. Simulating a GUI tool changing the value
    std::cout << "\n--- Simulating External GUI Configurator ---\n";
    {
        // Explicitly create a different originator
        cci::cci_originator gui_originator("Eclipse_Debug_GUI");

        h_mac.set_cci_value(cci::cci_value("11:22:33:44:55:66"), gui_originator);
    }

    // 5. Simulating the hardware modifying itself
    eth0.reset_mac_hardware();

    return 0;
}
```

### Why Originators Matter for Tooling

In a 100-million-gate SoC simulation, a parameter like `top.cpu0.cache.disable_prefetch` might be modified by:
1. The firmware writing to a memory-mapped register (Originator: `top.cpu0.iss`).
2. A TCL script executing during elaboration (Originator: `TCL_Script_Engine`).
3. A backend coverage tool enabling trace (Originator: `Coverage_Tool`).

When debugging why the simulation performance suddenly dropped, the audit log powered by originators is the only way to prove definitively *who* disabled the prefetcher.

## Introspection Tools

Because all parameters are registered centrally, you can build powerful introspection tools. A standard configuration dump tool will iterate over `broker.get_param_handles()`, querying each handle for:
- `name()`
- `get_cci_value().to_json()`
- `get_description()`
- `is_locked()`
- `get_value_origin().name()`

This single block of introspection code can dynamically generate HTML documentation or a comprehensive JSON dump for any standard-compliant VP, without requiring custom APIs from IP vendors.

## Lesson 67: CCI Values, JSON, and Metadata

Canonical lesson: https://www.learn-systemc.com/tutorials/065-cci-values-json-and-metadata

Using cci_value categories, JSON conversion, metadata maps, and tool-friendly configuration files.

## How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

# CCI Values, JSON, and Metadata

In a complex Virtual Platform, the configuration dataset is often managed externally using JSON files. The IEEE 1666.1 CCI standard is uniquely designed to interface cleanly with text-based formats through the `cci_value` variant class and its built-in JSON conversion utilities.

Furthermore, parameters can store arbitrary **Metadata**â€”extra facts about the parameter that are useful for configuration tools but not strictly required by the C++ simulation logic.

## Standard and source context

## Why `cci_value` Exists

A GUI, a command-line parser, or a JSON file cannot hold a C++ template instance like `cci_param<int>&`. External tools require a dynamic, type-erased value format. `cci::cci_value` provides that bridge, offering standard categories:
- Null (`cci::CCI_NULL_VALUE`)
- Boolean (`cci::CCI_BOOL_VALUE`)
- Integer / Real (`cci::CCI_INTEGRAL_VALUE`, `cci::CCI_REAL_VALUE`)
- String (`cci::CCI_STRING_VALUE`)
- Lists and Maps (`cci::CCI_LIST_VALUE`, `cci::CCI_DICT_VALUE`)

## Attaching Metadata

Metadata is a dictionary (map) of `cci_value` instances attached to a parameter. A modeler can expose facts such as:
- **units**: `"ns"`, `"bytes"`, `"Hz"`
- **valid ranges**: minimum and maximum bounds.
- **UI categories**: `"performance_knobs"`, `"address_map"`
- **Documentation links**: URLs pointing to hardware specifications.

Metadata makes the model "tool-friendly," allowing a GUI to automatically render a slider between `min` and `max` limits without hardcoding those rules in the GUI itself.

## Under the Hood: The Metadata Map

In the Accellera implementation, metadata is not a special class; it leverages the exact same `cci_value` AST engine used for configuration values. Inside the `cci_param_untyped` base class, there is a dedicated `std::map<std::string, cci_value> m_metadata` (or a `cci_value` object initialized to the `CCI_DICT_VALUE` category).

When a module calls `baud_rate.add_metadata("unit", cci::cci_value("bps"))`, it inserts this key-value pair directly into the parameter's internal dictionary. When an external tool calls `h_baud.get_metadata()`, the API returns a copy of this `cci_value` dictionary. Because it returns a standard `cci_value`, an external tool can easily call `get_metadata().to_json()` to instantly dump the entire metadata schema as a JSON object.

It is important to note that metadata is designed to be defined by the model *owner* during instantiation. External tools retrieving handles receive metadata by value (or const reference), preventing them from arbitrarily mutating the parameter's structural documentation at runtime.

## Complete Example: JSON Parsers and Metadata Extraction

The following complete example demonstrates how to attach metadata to a parameter, and simulates a top-level JSON configuration tool that parses a JSON string, applies presets, and extracts metadata for a mock User Interface.

```cpp
#include <systemc>
#include <cci_configuration>
#include <iostream>
#include <string>

class UARTController : public sc_core::sc_module {
public:
    cci::cci_param<int> baud_rate;
    cci::cci_param<bool> enable_parity;

    SC_HAS_PROCESS(UARTController);
    UARTController(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , baud_rate("baud_rate", 9600, "Baud rate of the UART")
        , enable_parity("enable_parity", false, "Enable parity checking")
    {
        // Attach Metadata to assist external tools
        baud_rate.add_metadata("unit", cci::cci_value("bps"));
        baud_rate.add_metadata("min", cci::cci_value(1200));
        baud_rate.add_metadata("max", cci::cci_value(115200));
        baud_rate.add_metadata("ui_category", cci::cci_value("Timing"));
    }

    void print_status() {
        std::cout << "[UART] Initialized with Baud: " << baud_rate.get_value()
                  << ", Parity: " << (enable_parity.get_value() ? "ON" : "OFF") << "\n";
    }
};

// Mock function simulating an external JSON Configuration Tool
void run_json_configurator(cci::cci_broker_handle broker) {
    std::cout << "--- JSON Tool Loading Presets ---\n";

    // A mock JSON string read from a config file
    std::string config_json = R"({
        "baud": 115200,
        "parity": true
    })";

    // 1. Parse JSON into a cci_value map
    cci::cci_value parsed = cci::cci_value::from_json(config_json);

    if (parsed.is_map()) {
        cci::cci_value::const_map_reference vmap = parsed.get_map();

        // 2. Set presets dynamically based on the JSON keys
        if (vmap.has_entry("baud")) {
            broker.set_preset_cci_value("top.uart.baud_rate", vmap.at("baud"));
            std::cout << "Set preset for baud_rate from JSON.\n";
        }
        if (vmap.has_entry("parity")) {
            broker.set_preset_cci_value("top.uart.enable_parity", vmap.at("parity"));
            std::cout << "Set preset for enable_parity from JSON.\n";
        }
    }
}

// Mock function simulating a GUI extracting Metadata to draw a slider
void run_gui_metadata_extractor(cci::cci_broker_handle broker) {
    std::cout << "\n--- GUI Tool Extracting Metadata ---\n";

    cci::cci_param_untyped_handle h_baud = broker.get_param_handle("top.uart.baud_rate");

    if (h_baud.is_valid()) {
        // Extract the metadata map
        cci::cci_value meta_val = h_baud.get_metadata();

        if (meta_val.is_map()) {
            cci::cci_value::const_map_reference meta = meta_val.get_map();

            std::cout << "Rendering UI for '" << h_baud.name() << "':\n";
            std::cout << "  Description: " << h_baud.get_description() << "\n";

            if (meta.has_entry("min") && meta.has_entry("max")) {
                std::cout << "  -> Drawing Slider from "
                          << meta.at("min").get_int() << " to "
                          << meta.at("max").get_int();

                if (meta.has_entry("unit")) {
                    std::cout << " " << meta.at("unit").get_string();
                }
                std::cout << "\n";
            }
        }
    }
}

int sc_main(int argc, char* argv[]) {
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
    cci::cci_broker_handle broker = cci::cci_get_broker();

    // 1. External tool runs BEFORE module instantiation to set presets
    run_json_configurator(broker);

    // 2. Hierarchy is elaborated. Modules consume presets.
    struct Top : public sc_core::sc_module {
        UARTController uart;
        Top(sc_core::sc_module_name n) : sc_core::sc_module(n), uart("uart") {}
    };
    Top top("top");

    // 3. Print the internal state to verify JSON was applied
    std::cout << "\n--- Internal State ---\n";
    top.uart.print_status();

    // 4. GUI tool runs to extract metadata
    run_gui_metadata_extractor(broker);

    return 0;
}
```

### Conversion Failures

When using `cci_value::from_json()`, invalid JSON syntax will throw a `cci::cci_value_failure` exception. When extracting data from a `cci_value` (e.g., using `.get_int()`), if the underlying variant does not hold an integer, an exception is also thrown.

The LRM specifies that `cci_value` operations strictly refuse implicit type coercion (e.g., extracting `"123"` as an integer). When writing tools, treat conversion failures as user-facing configuration errors, catch the exceptions gracefully, and report the specific JSON formatting mistake to the user, rather than allowing the SystemC kernel to crash.

## Lesson 68: Custom User-Defined Types (UDT) in CCI

Canonical lesson: https://www.learn-systemc.com/tutorials/066-custom-user-defined-types-udt-in-cci

How to safely wrap your custom hardware C++ types into cci_value and register them globally using SystemC CCI.

## How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

# Custom User-Defined Types in CCI

While SystemC Configuration Control Interface (CCI) provides native serialization for built-in C++ and SystemC types (like `int`, `std::string`, `sc_time`), you will often need to expose complex struct-based configurations to the broker. This requires bridging your User-Defined Type (UDT) with `cci_value`.

## Standard and source context

## The `cci_value` Bridge

To use a custom struct with a `cci_param`, you must define how that struct converts to and from a `cci_value`. The Accellera CCI implementation uses a template traits mechanism (`cci_param_traits`). By implementing the `cci_value_converter` template specialization, the CCI parameter registry will dynamically synthesize conversion calls when parsing broker data.

```cpp
#include <systemc>
#include <cci_configuration>
#include <string>

// 1. Define your custom hardware configuration struct
struct DMAConfig {
    unsigned int base_address;
    bool enable_scatter_gather;
    std::string channel_mode;

    // Equality operator required by CCI for tracking parameter value changes
    bool operator==(const DMAConfig& o) const {
        return base_address == o.base_address &&
               enable_scatter_gather == o.enable_scatter_gather &&
               channel_mode == o.channel_mode;
    }
};

// 2. Specialize cci_value_converter inside the cci namespace
namespace cci {
    template<>
    struct cci_value_converter<DMAConfig> {

        // Pack: UDT -> cci_value
        // cci_value is internally an AST. We tell it to become a map (JSON object)
        static bool pack(cci_value::reference dst, const DMAConfig& src) {
            dst.set_map()
               .push_back("base_address", cci_value(src.base_address))
               .push_back("enable_scatter_gather", cci_value(src.enable_scatter_gather))
               .push_back("channel_mode", cci_value(src.channel_mode));
            return true;
        }

        // Unpack: cci_value -> UDT
        // Safely extract from the AST back into C++ memory
        static bool unpack(DMAConfig& dst, cci_value::const_reference src) {
            if (!src.is_map()) return false;

            if (src.has_entry("base_address"))
                dst.base_address = src.get_entry("base_address").get_int();
            if (src.has_entry("enable_scatter_gather"))
                dst.enable_scatter_gather = src.get_entry("enable_scatter_gather").get_bool();
            if (src.has_entry("channel_mode"))
                dst.channel_mode = src.get_entry("channel_mode").get_string();

            return true;
        }
    };
} // namespace cci

// 3. Use it in a module
SC_MODULE(DMACtrl) {
    cci::cci_param<DMAConfig> cfg;

    SC_CTOR(DMACtrl)
      : cfg("cfg", DMAConfig{0x0000, false, "SINGLE"})
    {
        SC_METHOD(print_cfg);
    }

    void print_cfg() {
        DMAConfig val = cfg.get_value();
        std::cout << "DMA initialized at 0x" << std::hex << val.base_address << std::endl;
    }
};

int sc_main(int argc, char* argv[]) {
    cci::cci_register_broker(new cci_utils::broker("global_broker"));
    DMACtrl dma("dma");

    // Override from global broker using JSON
    cci::cci_get_broker().set_preset_cci_value("dma.cfg",
        cci::cci_value::from_json("{\"base_address\": 4096, \"enable_scatter_gather\": true, \"channel_mode\": \"BURST\"}"));

    sc_core::sc_start();
    return 0;
}
```

## Behind the Scenes: The RapidJSON AST

When `cci_value::from_json()` is called, the Accellera CCI proof-of-concept uses an embedded version of the RapidJSON library to parse the string.

It constructs a `cci_value`, which acts as a variant tree (an Abstract Syntax Tree of configuration). The `cci_broker_if` (the global configuration registry) stores this raw AST. The broker has zero knowledge of your `DMAConfig` struct.

Later, when your module instantiates `cci::cci_param<DMAConfig> cfg`, the parameter constructor queries the broker for the path `"dma.cfg"`. The broker returns a reference to the `cci_value` AST node. The template parameter triggers the compiler to locate `cci_value_converter<DMAConfig>::unpack()`. This static function bridges the untyped JSON tree into your strongly typed C++ memory space, allowing the simulation to proceed securely with zero dynamic type-checking overhead during execution.

## Lesson 69: CCI Presets, Configuration Files, and Unconsumed Values

Canonical lesson: https://www.learn-systemc.com/tutorials/067-cci-presets-configuration-files-and-unconsumed-val

How CCI preset values flow through brokers, how typos survive until checked, and how to build robust configuration validation.

## How to Read This Lesson

CCI becomes valuable when configuration comes from outside the model: command lines, JSON files, scripts, GUIs, or regression systems. This lesson is about making that flow reliable enough for real virtual platforms.

## Standard and source context

Use `Docs/LRMs/SystemC_CCI_1_0_LRM.pdf` for parameter initialization, brokers, handles, values, and originators. In source, inspect `Accellera SystemC GitHub repository/cci/configuration/src/cci`, especially broker interfaces and the consuming broker implementation.

## The Preset Value Idea

A preset value is configuration that arrives before the target parameter exists.

That matters because C++ elaboration order is hierarchical. A top-level script may know:

```text
top.cpu.frequency_hz = 500000000
```

before the `cpu` module has constructed its `cci_param<unsigned long long> frequency_hz`.

The broker stores the preset. When the parameter is created, it asks the broker whether a preset exists for its name. If yes, the parameter starts with that value instead of its constructor default.

## The Typo Problem

What if the file says:

```text
top.cup.frequency_hz = 500000000
```

The broker can store that preset, but no parameter ever consumes it. Without validation, your simulation may run with the default CPU frequency and nobody notices until performance numbers look wrong.

That is why unconsumed preset checks are not optional in serious platforms.

## Configuration Validation Pattern

At the end of elaboration or start of simulation, ask the broker for unconsumed presets and report them:

```cpp
void end_of_elaboration() override {
  auto broker = cci::cci_get_global_broker(
      cci::cci_originator{name()});

  auto unused = broker.get_unconsumed_preset_values();
  for (const auto& item : unused) {
    std::ostringstream os;
    os << "unused CCI preset: " << item.first;
    SC_REPORT_WARNING("CCI_CONFIG", os.str().c_str());
  }
}
```

Your project may choose to make this fatal. For production regressions, fatal is usually better: a typo in configuration is not a harmless warning.

## How the Source Makes This Work

Broker implementations keep two categories of information:

- live parameter interfaces registered by constructed parameters
- preset values waiting to be consumed

When a parameter appears, the broker can move a matching preset from the unused category into the parameter's initial value path. The source implementation makes the lifecycle concrete: configuration is not a global variable lookup; it is a broker-mediated registration and consumption protocol.

## Review Checklist

- Are all external knobs represented as CCI parameters?
- Are preset files validated for unconsumed values?
- Are bad presets warnings or fatal errors by project policy?
- Are originators used so tools and logs show who changed a value?
- Do parameter descriptions include units, legal ranges, and default intent?

## Lesson 70: CCI API, Callback, Value, and Handle Field Guide

Canonical lesson: https://www.learn-systemc.com/tutorials/134-cci-api-callback-value-and-handle-field-guide

A per-symbol guide to CCI brokers, parameters, callbacks, values, handles, predicates, ranges, and source implementation names.

## How to Read This Lesson

CCI is not just a prettier way to store variables. It is a standard way to make configuration inspectable, typed, tool-visible, and lifecycle-aware. Read each API name by asking what it protects: value ownership, mutation timing, tool access, callback ordering, or error reporting.

## Standard and source context

Use `Docs/LRMs/SystemC_CCI_1_0_LRM.pdf`. Use `.codex-src/cci/cci`, `.codex-src/cci/configuration/src`, and `.codex-src/cci/inspection/src` for implementation reading.

## Broker and Naming APIs

`cci_broker_if`, `cci_broker_handle`, `cci_broker_handles`, `cci_register_broker`, `cci_get_broker`, `cci_gen_unique_name`, `cci_get_name`, `cci_unregister_name`, and `cci_name_gen` sit around broker discovery and unique naming.

Some source and generated documentation may contain the misspelled `cci_orginator`; read it as the originator concept, not as a separate modeling idea.

The broker is the configuration authority. It owns the map from hierarchical names to parameters and preset values. A `cci_broker_handle` gives client code a safer front door than directly poking the broker implementation.

`cci_originator` records who is asking. That matters because tools, modules, and processes may not all deserve the same permissions.

## Parameter Shape and Mutability

`cci_param`, `cci_param_if`, `cci_untyped_param`, `cci_untyped_tag`, `cci_typed_tag`, `cci_typed_handle`, `cci_param_mutable_type`, `cci_param_data_category`, and `cci_mutable_param` describe parameter typing and mutation policy.

Typed parameters are what model authors normally instantiate. Untyped access is what generic tools need when they enumerate a platform without knowing every C++ template parameter.

`cci_param_range`, `cci_filtered_range`, `cci_param_predicate`, `cci_preset_value_range`, and `cci_preset_value_predicate` are iteration and filtering concepts. Use them when tools need to ask, "show me every parameter matching this category or hierarchy."

## Presets and Unconsumed Values

Preset APIs let configuration arrive before the owning parameter exists. That is useful for top-level platform configuration, but it creates one classic failure mode: a typo becomes an unconsumed preset.

`cci_preset_value_predicate`, `cci_preset_value_range`, `get_unconsumed_preset_values`, and broker preset maps should be part of every VP bring-up checklist. A configuration file that silently falls back to defaults can waste days.

## Callback APIs

`cci_callback`, `cci_callback_typed_handle`, `cci_callback_traits`, `cci_callback_untyped_handle`, `cci_param_callback_if`, `cci_param_create_callback`, `cci_param_create_callback_handle`, `cci_param_destroy_callback`, `cci_param_destroy_callback_handle`, `cci_param_pre_read_callback`, `cci_param_pre_read_callback_typed`, `cci_param_pre_read_callback_untyped`, `cci_param_read_event`, and value-change callbacks let tools and model owners observe parameter lifecycle and access.

Write-path and post-read variants include `cci_param_pre_write_callback`, `cci_param_pre_write_callback_typed`, `cci_param_pre_write_callback_untyped`, `cci_param_post_write_callback`, `cci_param_post_write_callback_typed`, `cci_param_post_write_callback_untyped`, `cci_param_post_read_callback`, `cci_param_post_read_callback_typed`, `cci_param_post_read_callback_untyped`, `cci_param_pre_write_callback_handle`, `cci_param_post_write_callback_handle`, `cci_param_pre_read_callback_handle`, and `cci_param_post_read_callback_handle`.

Callbacks are powerful because they turn configuration into an observable protocol. They are dangerous when they hide side effects. Keep callbacks small, documented, and explicit about whether they run during elaboration or simulation.

## Value Tree APIs

`cci_value`, `cci_value_ref`, `cci_value_cref`, `cci_value_list_ref`, `cci_value_list_cref`, `cci_value_map`, `cci_value_map_ref`, `cci_value_map_cref`, `cci_value_map_elem_ref`, `cci_value_map_elem_cref`, `cci_value_string_ref`, `cci_value_string_cref`, `cci_value_iterator`, and `cci_name_value_pair` form the JSON-like value tree.

This is what lets CCI represent structured configuration instead of only scalar C++ values. In a real platform, that can mean a table of memory regions, a map of feature switches, or a list of debug endpoints.

## Error and Report APIs

`cci_param_failure`, `cci_report_handler`, `cci_abort`, and `cci_handle_exception` belong to the error path. Good CCI code does not just return false and hope somebody reads a log. It reports configuration mistakes early, preferably before simulation results become misleading.

## Implementation Names Worth Recognizing

`cci_impl`, `cci_cmnhdr`, `cci_broker_callback_if`, and internal handle classes are implementation scaffolding. You do not model with them directly, but recognizing them makes the source tree easier to navigate.

Other source names worth recognizing are `cci_get_cci_unique_names`, `cci_name_state`, `cci_value_has_converter`, `cci_value_unpack_fx`, `cci_config_macros`, `cci_msg_type`, `cci_msg_types`, `cci_msg_type_prefix`, `cci_msg_type_prefix_len`, `cci_meta`, `cci_core_types`, `cci_mutable_types`, `cci_tag`, `cci_callback_impl`, `cci_broker_callbacks`, `cci_broker_types`, `cci_param_callbacks`, `cci_value_converter_disabled`, `cci_inspection`, `cci_param_cast`, `cci_true_type`, and `cci_false_type`. Most are implementation support for type traits, message IDs, conversion, inspection, and callback dispatch.

The remaining utility names `cci_name_free`, `cci_name_used`, `cci_value_delegate_converter`, `cci_version`, `cci_macros_undef`, `cci_param_predicate_handle`, `cci_preset_value_predicate_handle`, `cci_iterable`, `cci_param_write_event_untyped`, and `cci_param_read_event_untyped` fit the same pattern: naming state, converter delegation, version metadata, macro cleanup, iterable ranges, predicate handles, and untyped read/write events.

## Senior Review Checklist

- Is every structural parameter locked before simulation?
- Are unconsumed presets checked?
- Can generic tooling inspect values without knowing C++ template types?
- Are callbacks simple enough to reason about?
- Do errors explain the parameter path, originator, and attempted value?

## Lesson 71: System Verification, SCV & UVM-SystemC

Canonical lesson: https://www.learn-systemc.com/tutorials/068-system-verification-scv-and-uvm-systemc

Moving from simple testbenches to robust verification using the SystemC Verification Library (SCV) and UVM-SystemC.

## How to Read This Lesson

# System Verification & UVM-SystemC

When building simple models, a basic `sc_main` with a few `std::cout` statements and `assert()` calls is sufficient. However, for industrial System-on-Chips (SoCs), you need robust verification environments capable of constrained random stimulus, functional coverage, and automated checking.

This is where the **SystemC Verification Library (SCV)** and the IEEE standard **UVM-SystemC** come into play.

## Standard and source context

## The SystemC Verification Library (SCV)

SCV is an Accellera standard library built on top of SystemC. It provides three main features:
1. **Data Introspection:** Dynamically inspecting the fields of a C++ struct or class using macro-generated trait classes.
2. **Constrained Randomization:** Generating random data that mathematically adheres to complex hardware constraints.
3. **Transaction Recording:** Automatically dumping TLM generic payloads to a database for waveform viewing (like GTKWave).

**Under the Hood (Accellera SCV source):**
When you use `scv_constraint`, the SCV kernel transforms your C++ boolean expressions (like `p->address() >= 0x1000`) into an Abstract Syntax Tree (AST). It then passes this AST to its internal constraint solver, which traditionally uses Binary Decision Diagrams (BDDs) to find a valid mathematical solution space before picking a random vector. `scv_smart_ptr` handles the user-facing API by heavily overloading `operator->` and implicitly managing memory.

### Complete SCV Constrained Randomization Example

In standard C++, `rand()` generates uniform distributions. SCV introduces `scv_smart_ptr` and `scv_constraint` to build powerful declarative generators similar to SystemVerilog's `randc`.

```cpp
#include <systemc>
#include <scv.h>

// 1. Define a standard C++ struct representing a Bus Packet
struct Packet {
    int address;
    int payload[4];
};

// 2. Define introspection (so SCV knows the memory layout)
SCV_EXTENSIONS(Packet) {
public:
    scv_extensions<int> address;
    scv_extensions<int[4]> payload;
    SCV_EXTENSIONS_CTOR(Packet) {
        SCV_FIELD(address);
        SCV_FIELD(payload);
    }
};

// 3. Create a constraint class
class PacketConstraint : public scv_constraint_base {
public:
    scv_smart_ptr<Packet> p;

    SCV_CONSTRAINT_CTOR(PacketConstraint) {
        // Constrain address between 0x1000 and 0x2000
        SCV_CONSTRAINT( p->address() >= 0x1000 && p->address() <= 0x2000 );

        // Constrain payload values to be strictly positive
        for(int i = 0; i < 4; i++) {
            SCV_CONSTRAINT( p->payload()[i] > 0 );
        }
    }
};

SC_MODULE(Testbench) {
    SC_CTOR(Testbench) {
        SC_THREAD(generator_thread);
    }

    void generator_thread() {
        PacketConstraint gen("gen");
        std::cout << "--- Generating Constrained Random Packets ---" << std::endl;

        for(int i = 0; i < 5; i++) {
            gen.next(); // Solves constraints and generates a new packet
            std::cout << "Packet " << i << " | Addr: 0x" << std::hex << gen.p->address()
                      << " | Payload[0]: " << std::dec << gen.p->payload()[0] << std::endl;
        }
    }
};

int sc_main(int argc, char* argv[]) {
    Testbench tb("tb");
    sc_core::sc_start();
    return 0;
}
```

## UVM-SystemC

The Universal Verification Methodology (UVM) is widely used for verifying RTL logic using SystemVerilog. **UVM-SystemC** is an Accellera public-review library and draft methodology for bringing familiar UVM architecture concepts, including phases, components, configuration databases, and TLM connections, into SystemC/C++.

**Under the Hood (Accellera `uvm-systemc` repository):**
UVM-SystemC is intricately tied to the SystemC discrete-event kernel:
- `uvm_component` directly inherits from `sc_core::sc_module`.
- The UVM Factory (`UVM_COMPONENT_UTILS`) uses static class initialization and C++ RTTI to register class string names ("MyDriver") into a global `uvm_factory` singleton map before `sc_main` even executes.
- The UVM Phasing mechanism (like `build_phase`, `run_phase`) bridges with `sc_simcontext`. The `run_phase` does not use `SC_THREAD` directly in the constructor. Instead, the UVM phase manager issues an `sc_spawn` during simulation to dynamically create an `sc_thread_process` that wraps your `run_phase` method. It synchronizes across components using `sc_event` phase barriers.

### UVM-SystemC Architecture Example

Just like SystemVerilog UVM, you build components derived from standard base classes like `uvm_driver`, `uvm_monitor`, and `uvm_scoreboard`.

```cpp
#include <systemc>
#include <uvm>

class MyTransaction : public uvm::uvm_sequence_item {
public:
    int data;
    UVM_OBJECT_UTILS(MyTransaction);
    MyTransaction(const std::string& name = "MyTransaction") : uvm::uvm_sequence_item(name), data(0) {}
};

class MyDriver : public uvm::uvm_driver<MyTransaction> {
public:
    UVM_COMPONENT_UTILS(MyDriver);

    MyDriver(uvm::uvm_component_name name) : uvm::uvm_driver<MyTransaction>(name) {}

    // The run phase executes as a coroutine spawned by the UVM phase manager
    void run_phase(uvm::uvm_phase& phase) {
        while(true) {
            MyTransaction req;

            // In a real environment, this blocks until a sequencer sends an item
            // seq_item_port->get_next_item(req);

            UVM_INFO("DRIVER", "Driving transaction to DUT...", uvm::UVM_LOW);
            wait(10, sc_core::SC_NS); // Simulate driving delay on the bus

            // seq_item_port->item_done();

            // Break to avoid infinite loop in this mock example
            break;
        }
    }
};

int sc_main(int argc, char* argv[]) {
    // In a real UVM environment, you would call uvm::run_test();
    // This example isolates the UVM driver for structural clarity.

    MyDriver driver("driver");
    sc_core::sc_start(50, sc_core::SC_NS);
    return 0;
}
```

## Summary

If you are building a quick prototype, standard `sc_main` C++ testing is fine. But for production-grade IP modeling, leveraging **SCV** for constrained randomization and **UVM-SystemC** for hierarchical testbench architecture ensures your models are rigorously verified and interoperable across the hardware engineering lifecycle.

## Lesson 72: UVM-SystemC Factory and Config Internals

Canonical lesson: https://www.learn-systemc.com/tutorials/069-uvm-systemc-factory-and-config-internals

How UVM-SystemC uses factories, type IDs, component hierarchy, config_db, and resource lookup to build reusable verification.

## How to Read This Lesson

# UVM-SystemC Factory and Config Internals

UVM-SystemC gives verification code a standard architecture: components, objects, factory creation, configuration, phases, reports, and TLM communication. The core of this flexibility is the **UVM Factory**.

## Standard and source context

## Factory Purpose

The factory lets a test replace one type with another without rewriting the whole environment. This relies heavily on the macros `UVM_COMPONENT_UTILS` and `UVM_OBJECT_UTILS`, which register string names and type IDs with a centralized factory singleton.

Typical uses:
- replace a driver with an error-injecting driver
- replace a monitor with a coverage monitor
- choose a sequence item subtype
- configure tests through type overrides

### Under the Hood: C++ Implementation in Accellera UVM-SystemC

How does the UVM factory actually instantiate C++ classes dynamically by string name or override rules? In C++, dynamic creation without reflection requires a robust boilerplate pattern. The Accellera repository implements this in `src/uvmsc/base/uvm_factory.*`.

1. **The Registry Pattern:** When you use `UVM_COMPONENT_UTILS(my_driver)`, the macro injects a static registry class inside `my_driver`. This nested class (`type_id`) registers a proxy creator object with the global `uvm_factory` singleton during the C++ static initialization phase (before `sc_main` even begins!).
2. **Override Queues:** The `uvm_factory` maintains two primary structures: `m_type_overrides` and `m_inst_overrides`. When `my_driver::type_id::create("drv", this)` is called, the factory intercepts the call, searches its override queues using the hierarchical path string and requested type string, and determines if a substitution is required.
3. **The Proxy Creator:** Once the final type is resolved, the factory invokes a virtual `create_component()` method on the proxy object registered by the target class. This method simply calls `new error_driver(name)` under the hood and returns the base `uvm_component` pointer.

## Factory Overrides Example

Below is a complete, fully compilable `sc_main` example showing how to use the factory to perform a type override. We will define a `base_driver` and an `error_driver` that inherits from it, and then instruct the factory to swap them out at runtime.

```cpp
#include <systemc>
#include <uvm>

// A dummy transaction
class my_transaction : public uvm::uvm_transaction {
public:
    UVM_OBJECT_UTILS(my_transaction);
    my_transaction(const std::string& name = "my_transaction") : uvm::uvm_transaction(name) {}
};

// 1. Define a base driver
class base_driver : public uvm::uvm_driver<my_transaction> {
public:
    UVM_COMPONENT_UTILS(base_driver);

    base_driver(uvm::uvm_component_name name) : uvm::uvm_driver<my_transaction>(name) {}

    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);
        UVM_INFO("DRV", "Base driver executing NORMAL behavior", uvm::UVM_LOW);
        sc_core::wait(10, sc_core::SC_NS);
        phase.drop_objection(this);
    }
};

// 2. Define an error-injecting driver that inherits from the base
class error_driver : public base_driver {
public:
    UVM_COMPONENT_UTILS(error_driver);

    error_driver(uvm::uvm_component_name name) : base_driver(name) {}

    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);
        UVM_ERROR("DRV_ERR", "Error driver executing FAULTY behavior");
        sc_core::wait(10, sc_core::SC_NS);
        phase.drop_objection(this);
    }
};

// 3. Define an environment that instantiates the base driver
class my_env : public uvm::uvm_env {
public:
    base_driver* drv;
    UVM_COMPONENT_UTILS(my_env);

    my_env(uvm::uvm_component_name name) : uvm::uvm_env(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_env::build_phase(phase);

        // We MUST use the factory to create it, otherwise overrides won't work!
        drv = base_driver::type_id::create("drv", this);
    }
};

// 4. Define a test that sets up the factory override
class my_test : public uvm::uvm_test {
public:
    my_env* env;
    UVM_COMPONENT_UTILS(my_test);

    my_test(uvm::uvm_component_name name) : uvm::uvm_test(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_test::build_phase(phase);

        // OVERRIDE: Tell the factory that whenever 'base_driver' is requested,
        // it should instantiate 'error_driver' instead.
        base_driver::type_id::set_type_override(error_driver::get_type());

        // Now when the env creates 'drv', it will actually be an 'error_driver'
        env = my_env::type_id::create("env", this);
    }

    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);
        UVM_INFO("TEST", "Printing topology to verify override:", uvm::UVM_LOW);
        uvm::uvm_root::get()->print_topology();
        phase.drop_objection(this);
    }
};

int sc_main(int argc, char* argv[]) {
    // Start the test
    uvm::run_test("my_test");
    return 0;
}
```

## Source Implementation Shape

If you are exploring the UVM-SystemC implementation according to the IEEE 1800.2 standard, useful official source reading paths include:

- `src/uvmsc/base/uvm_component.*`
- `src/uvmsc/base/uvm_object.*`
- `src/uvmsc/base/uvm_factory.*`
- `src/uvmsc/conf/uvm_config_db.h`
- `src/uvmsc/conf/uvm_resource_pool.*`

The config database is built on resource lookup ideas. That explains why names, scopes, and wildcard patterns matter so much.

## Best Practice

- Always construct dynamic components using `type_id::create` rather than C++ `new`. If you use `new`, the factory is bypassed and overrides will fail.
- Use config DB for testbench configuration, not for every transaction.
- Retrieve configuration in build/connect phases and store it in clear member variables.
- Avoid broad wildcard settings unless the intent is truly global.

## Relation to CCI

CCI is better for model configuration and tool inspection. UVM config DB is better for verification environment configuration.

In a combined project, use CCI to configure the DUT or VP model and UVM config DB to configure agents, monitors, sequences, and scoreboards.

## Accellera Source Implementation: `uvm_factory::create`

When you call `my_comp::type_id::create("inst", this)`, it routes through the global UVM factory. In `src/uvmsc/base/uvm_factory.cpp`, the factory resolves type overrides.

```cpp
// Abstract representation of uvm_factory::create_component_by_type
uvm_component* uvm_factory::create_component_by_type(
    const uvm_object_wrapper* requested_type,
    const std::string& parent_inst_path,
    const std::string& name)
{
    // 1. Check for overrides (Type or Instance)
    const uvm_object_wrapper* resolved_type = find_override(requested_type, parent_inst_path);

    // 2. Instantiate via Policy Class (Object Proxy)
    uvm_component* comp = resolved_type->create_component(name, parent_inst_path);

    return comp;
}
```
The `find_override` method scans a strict priority queue of user-defined overrides. If a testbench registered a mock object (`factory.set_type_override_by_type(...)`), the factory returns the proxy for the mock object instead of the original, achieving polymorphic dependency injection cleanly.

## Lesson 73: UVM Phasing, Objections, and the SystemC Kernel

Canonical lesson: https://www.learn-systemc.com/tutorials/070-uvm-phasing-objections-and-the-systemc-kernel

How UVM phases run on top of SystemC, why objections control phase completion, and how run-time behavior maps to processes.

## How to Read This Lesson

# UVM Phasing, Objections, and the SystemC Kernel

UVM phasing gives a verification environment a shared lifecycle. SystemC gives it the simulation kernel. UVM-SystemC sits on top of SystemC and organizes verification behavior into phases. According to the IEEE 1800.2 standard, the UVM phasing mechanism is a sophisticated state machine layered over the native SystemC scheduler.

## Standard and source context

## Phase Families

Common phase ideas include:

- build
- connect
- end of elaboration
- start of simulation
- run
- extract
- check
- report

Build and connect are structural. Run is time-consuming. Check and report summarize results.

### Under the Hood: C++ Implementation in Accellera UVM-SystemC

The bridge between UVM phasing and the SystemC kernel is a fascinating piece of engineering found in `src/uvmsc/phasing/uvm_phase.*` and `src/uvmsc/base/uvm_root.*`.

1. **SystemC Callbacks:** The singleton `uvm_root` registers itself with the SystemC kernel's standard callbacks. During SystemC's `end_of_elaboration`, `uvm_root` initiates the `build_phase`, `connect_phase`, and `end_of_elaboration_phase` traversals across the UVM component tree.
2. **`sc_spawn` the Run Phase:** When `sc_start()` is called and SystemC enters the evaluation phase, the UVM phase state machine transitions to the `run_phase`. To allow multiple components to consume time independently, the UVM kernel iterates over every `uvm_component` and uses SystemC's dynamic process generation `sc_spawn` to turn their virtual `run_phase()` methods into independent `SC_THREAD`s.
3. **Objection State Machine:** Objections are implemented as atomic reference counters (`uvm_objection`). The UVM kernel spawns a dedicated monitor process that sleeps using `sc_core::wait()` on an event that fires whenever an objection drops to zero. When all objections hit zero, this monitor process wakes up, forcibly terminates all spawned `run_phase` threads using `sc_process_handle::kill()`, and transitions the state machine to the `extract_phase`.

## Objections

Because the `run_phase` executes concurrently in every component across the hierarchy (using `sc_spawn` under the hood), UVM requires a mechanism to determine when the entire simulation is "done".

Objections prevent a run-time phase from ending while useful activity is still happening.
If every component drops its objection, the phase can complete.

## Complete Objections Example

Below is a fully compilable `sc_main` example showing two components running concurrently in the `run_phase`. Notice how the test waits until both components drop their objections before moving to the `report_phase`.

```cpp
#include <systemc>
#include <uvm>

// 1. Fast Component: Finishes its work quickly
class fast_worker : public uvm::uvm_component {
public:
    UVM_COMPONENT_UTILS(fast_worker);
    fast_worker(uvm::uvm_component_name name) : uvm::uvm_component(name) {}

    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);
        UVM_INFO("FAST", "Starting fast work (takes 20 ns)", uvm::UVM_LOW);

        sc_core::wait(20, sc_core::SC_NS);

        UVM_INFO("FAST", "Finished fast work, dropping objection", uvm::UVM_LOW);
        phase.drop_objection(this);
    }
};

// 2. Slow Component: Keeps the simulation alive longer
class slow_worker : public uvm::uvm_component {
public:
    UVM_COMPONENT_UTILS(slow_worker);
    slow_worker(uvm::uvm_component_name name) : uvm::uvm_component(name) {}

    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);
        UVM_INFO("SLOW", "Starting slow work (takes 100 ns)", uvm::UVM_LOW);

        sc_core::wait(100, sc_core::SC_NS);

        UVM_INFO("SLOW", "Finished slow work, dropping objection", uvm::UVM_LOW);
        phase.drop_objection(this);
    }
};

// 3. Test Environment
class my_test : public uvm::uvm_test {
public:
    fast_worker* fast;
    slow_worker* slow;
    UVM_COMPONENT_UTILS(my_test);

    my_test(uvm::uvm_component_name name) : uvm::uvm_test(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_test::build_phase(phase);
        fast = fast_worker::type_id::create("fast", this);
        slow = slow_worker::type_id::create("slow", this);
    }

    void report_phase(uvm::uvm_phase& phase) override {
        // This is called automatically in zero-time AFTER all run_phase
        // objections have been dropped.
        UVM_INFO("TEST", "Report Phase reached. Total simulation time: "
                 + sc_core::sc_time_stamp().to_string(), uvm::UVM_LOW);
    }
};

int sc_main(int argc, char* argv[]) {
    uvm::run_test("my_test");
    return 0;
}
```

## Source Implementation Shape

Useful official source paths include:

- `src/uvmsc/phasing/uvm_phase.*`
- `src/uvmsc/phasing/uvm_common_phases.*`
- `src/uvmsc/phasing/uvm_objection.*`
- `src/uvmsc/base/uvm_root.*`

The phasing system traverses component hierarchy and calls phase callbacks. Run-time phases interact with SystemC processes and simulation time.

## Common Mistake

Starting a sequence without raising an objection can let the phase end too early.

Raising an objection and never dropping it can make the simulation appear hung. This happens frequently if an exception is thrown or a state machine gets deadlocked before reaching the `drop_objection` call.

Both failures are lifecycle bugs, not TLM bugs.

## Best Practice

Raise objections at the level that owns the activity (e.g., inside sequences or tests, rather than deep in monitors). Drop them in all exit paths. Use reports around objections when debugging phase completion.

For small examples, keep the objection pattern obvious rather than clever.

## Lesson 74: UVM Components and Hierarchy

Canonical lesson: https://www.learn-systemc.com/tutorials/071-uvm-components-and-hierarchy

Explore the foundation of UVM-SystemC testbenches using uvm_component and the UVM hierarchy.

## How to Read This Lesson

Welcome to the UVM Deep Dive! In this chapter, we explore the core building blocks of the Universal Verification Methodology in SystemC (UVM-SystemC). If you've written plain SystemC testbenches, you'll immediately see how UVM brings standardized structure and reusability to your verification environments. According to the IEEE 1800.2 standard for UVM, components form the static hierarchy of the verification environment.

## Standard and source context

## The Foundation: `uvm_component`

In UVM-SystemC, the structural foundation of your testbench is built using classes derived from `uvm_component`. While transient data like transactions derive from `uvm_object`, static architectural piecesâ€”like drivers, monitors, and scoreboardsâ€”derive from `uvm_component`.

### Under the Hood: C++ Implementation in Accellera UVM-SystemC

To truly master `uvm_component`, it helps to understand how the Accellera `uvm-systemc` repository implements it. If you browse the official source code, you'll see the following class inheritance:

```cpp
class uvm_component : public uvm_report_object,
                      public sc_core::sc_module {
    // ...
};
```

This multiple inheritance is the magic bridge between UVM and SystemC.

1. **`sc_core::sc_module`**: By inheriting from `sc_module`, a `uvm_component` registers itself into the SystemC `sc_simcontext` hierarchy. The SystemC kernel views your UVM environment as standard SystemC modules. This means UVM components can use `SC_THREAD`, `SC_METHOD`, and natively contain SystemC ports (`sc_in`, `sc_out`).
2. **`uvm_component_name`**: Notice that UVM component constructors require a `uvm_component_name` argument. In SystemC, module names are strictly managed by `sc_module_name` to build the hierarchy tree. UVM wraps this in `uvm_component_name` to allow dynamic instantiation via the factory during the `build_phase` while maintaining synchronization between the `uvm_root` hierarchy map and the internal `sc_object` tree.
3. **`uvm_report_object`**: This inheritance provides the `UVM_INFO`, `UVM_WARNING`, and `UVM_ERROR` logging infrastructure, inherently tracking the component's hierarchical path.

### Pre-defined Component Types

Rather than directly extending `uvm_component` for everything, UVM provides semantic base classes. While they function identically to `uvm_component`, they clarify the *intent* of your architecture:

*   **`uvm_env`**: The top-level container for a verification environment.
*   **`uvm_agent`**: Encapsulates a sequencer, driver, and monitor for a specific protocol.
*   **`uvm_driver`**: Requests transactions from a sequencer and drives them onto the physical DUT interface.
*   **`uvm_monitor`**: Passively samples the DUT interface and broadcasts transactions via analysis ports.
*   **`uvm_scoreboard`**: Checks predicted behavior against actual DUT outputs.
*   **`uvm_test`**: The top-level test class that configures the environment and initiates stimulus.

## Constructing Components

When constructing a UVM component, you always provide a name to identify it in the UVM hierarchy. Thanks to the factory pattern (which we will cover later), components are typically instantiated dynamically rather than statically. The UVM standard dictates that components must be created using the factory `create` method during the `build_phase`.

Here is a complete, fully compilable example demonstrating how to define, build, and execute a component hierarchy using `uvm_component` and the factory.

```cpp
#include <systemc>
#include <uvm>

// A dummy transaction for the driver
class my_transaction : public uvm::uvm_sequence_item {
public:
    int data;
    UVM_OBJECT_UTILS(my_transaction);
    my_transaction(const std::string& name = "my_transaction")
        : uvm::uvm_sequence_item(name), data(0) {}
};

// 1. Define a UVM Driver
class my_driver : public uvm::uvm_driver<my_transaction> {
public:
    // UVM Component Registration Macro
    UVM_COMPONENT_UTILS(my_driver);

    // Standard UVM Component Constructor
    explicit my_driver(uvm::uvm_component_name name)
        : uvm::uvm_driver<my_transaction>(name) {
    }

    void run_phase(uvm::uvm_phase& phase) override {
        UVM_INFO("DRV", "Driver is running", uvm::UVM_LOW);
    }
};

// 2. Define a UVM Environment containing the Driver
class my_env : public uvm::uvm_env {
public:
    my_driver* driver;
    UVM_COMPONENT_UTILS(my_env);

    explicit my_env(uvm::uvm_component_name name) : uvm::uvm_env(name) {}

    // Components are instantiated in the build_phase
    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_env::build_phase(phase);

        // Use the UVM factory to create the driver
        driver = my_driver::type_id::create("driver", this);
        UVM_INFO("ENV", "Environment built driver component", uvm::UVM_LOW);
    }
};

// 3. Define the UVM Test containing the Environment
class my_test : public uvm::uvm_test {
public:
    my_env* env;
    UVM_COMPONENT_UTILS(my_test);

    explicit my_test(uvm::uvm_component_name name) : uvm::uvm_test(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_test::build_phase(phase);
        env = my_env::type_id::create("env", this);
    }

    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);
        UVM_INFO("TEST", "Running test...", uvm::UVM_LOW);
        sc_core::wait(10, sc_core::SC_NS); // Consuming simulation time

        // Print the component hierarchy
        uvm::uvm_root::get()->print_topology();

        phase.drop_objection(this);
    }
};

// 4. Standard SystemC Entry Point
int sc_main(int argc, char* argv[]) {
    // Initiate UVM phasing by starting the test
    uvm::run_test("my_test");
    return 0;
}
```

### The Component Hierarchy and `uvm_top`

Because UVM components are created dynamically (typically during the `build_phase`), UVM maintains its own internal hierarchy tree, distinct from (but mapped onto) the SystemC module hierarchy.

At the absolute root of this hierarchy sits a singleton called `uvm_root`. The UVM-SystemC implementation provides a global pointer to this singleton named `uvm_top`.
Any `uvm_component` instantiated without an explicit UVM parent automatically becomes a child of `uvm_top`.

You can traverse or search this hierarchy using built-in methods provided by `uvm_component`:
*   `get_parent()`: Returns a pointer to the component's parent.
*   `get_full_name()`: Returns the full hierarchical string path (e.g., `uvm_test_top.env.driver`).
*   `get_children()`: Returns a vector of pointers to the component's immediate children.

## Why use `uvm_component` instead of `sc_module`?

Since `uvm_component` inherits from `sc_module`, why use UVM at all? The answer lies in the **standardized interfaces** `uvm_component` introduces as defined by the Accellera standard:

1.  **Phasing:** Standardized lifecycle callbacks (like `build_phase`, `run_phase`) instead of manually relying solely on `sc_main` or `end_of_elaboration`.
2.  **Configuration:** Seamless integration with the UVM Configuration Database (`uvm_config_db`).
3.  **Reporting:** Standardized, hierarchically controllable logging (`UVM_INFO`, `UVM_ERROR`).
4.  **Factory:** Dynamic type overrides (replacing a base driver with an error-injecting driver without changing the testbench code).

In the next tutorial, we will look at the **UVM Phasing Mechanism**, which strictly controls how these components are constructed, connected, and executed.

## Lesson 75: The UVM Phasing Mechanism

Canonical lesson: https://www.learn-systemc.com/tutorials/072-the-uvm-phasing-mechanism

Learn how UVM phases structure the lifecycle of a testbench, from construction to execution and cleanup.

## How to Read This Lesson

In a standard SystemC simulation, you have elaboration (construction) and simulation (`sc_start()`). The Universal Verification Methodology (UVM) imposes a much more rigorous, standardized execution schedule on top of SystemC called the **Phasing Mechanism**.

Phases ensure that all components in the verification environment instantiate, connect, run, and shut down in a predictable, synchronized manner.

## Standard and source context

## The Three Categories of UVM Phases

Phases in UVM are executed sequentially. Every component in the hierarchy must complete a phase before the entire environment transitions to the next phase. The phases are divided into three main categories: **Pre-run**, **Run-time**, and **Post-run**.

### Under the Hood: The C++ Implementation in Accellera UVM-SystemC

How does the UVM kernel orchestrate these phases across the entire `uvm_component` tree? If you look inside the `uvm-systemc` repository, phasing is implemented as a sophisticated State Machine.

1.  **`uvm_phase` classes:** Every phase is represented by an object inheriting from `uvm_phase` (which itself derives from `uvm_object`). The kernel maintains a graph (domain) of these phase nodes.
2.  **Traversal Strategies:** For zero-time pre-run phases, the kernel executes a graph traversal. Pre-run phases map directly to SystemC's `end_of_elaboration` and `start_of_simulation` callbacks. Classes like `uvm_topdown_phase` (for `build_phase`) and `uvm_bottomup_phase` (for `connect_phase`) determine the order in which the kernel iterates over the `uvm_root` component hierarchy.
3.  **Run-Time Threads (`sc_spawn`):** When the simulation transitions into the `run_phase`, it enters the time-consuming domain. Under the hood, the UVM kernel calls SystemC's dynamic process generation `sc_spawn()` to launch the `run_phase()` of each component as an independent, concurrent `SC_THREAD`. Because they are standard SystemC threads, you can freely use `sc_core::wait()` to suspend execution.

### 1. Pre-run Phases (Zero Time)

Pre-run phases are used for structural setup. They execute in zero simulation time. In UVM-SystemC, these map conceptually to SystemC's elaboration steps.

*   **`build_phase`** (Top-down): This is where you instantiate your sub-components and retrieve configuration settings from the `uvm_config_db`. Because it executes top-down, parents can set configurations before their children are built.
*   **`connect_phase`** (Bottom-up): Once all components are built, TLM ports and exports are bound together here.
*   **`end_of_elaboration_phase`** (Bottom-up): Final structural adjustments and topology checks.
*   **`start_of_simulation_phase`** (Bottom-up): Pre-run activities like printing banners, dumping the testbench topology, or initializing debug files.

### 2. Run-time Phases (Consumes Time)

Run-time phases are where the actual simulation stimulus and protocol execution happen.

*   **`run_phase`**: This is the primary workhorse. Unlike the pre-run phases, `run_phase` is spawned as a concurrent **thread process** (using SystemC's `sc_spawn` under the hood). Every component's `run_phase` executes concurrently.

UVM also defines parallel sub-phases within the run-time domain (such as `reset_phase`, `configure_phase`, `main_phase`, and `shutdown_phase`), but `run_phase` is the most commonly used for general component logic.

### 3. Post-run Phases (Zero Time)

Once the run-time phases are explicitly terminated, the simulation moves into cleanup and checking.

*   **`extract_phase`** (Bottom-up): Retrieve final data from coverage collectors and scoreboards.
*   **`check_phase`** (Bottom-up): Validate the extracted data to determine if the test passed or failed.
*   **`report_phase`** (Bottom-up): Print the final results (e.g., "TEST PASSED" or coverage percentages).
*   **`final_phase`** (Top-down): Final teardown, like closing open file handles.

## Implementing a Phase

To participate in a phase, a component simply overrides the virtual method for that phase. Below is a complete, fully compilable example demonstrating all three categories of UVM phases.

```cpp
#include <systemc>
#include <uvm>

class my_transaction : public uvm::uvm_transaction {
public:
    UVM_OBJECT_UTILS(my_transaction);
    my_transaction(const std::string& name = "my_transaction") : uvm::uvm_transaction(name) {}
};

class my_monitor : public uvm::uvm_monitor {
public:
    UVM_COMPONENT_UTILS(my_monitor);

    uvm::uvm_analysis_port<my_transaction> ap;

    my_monitor(uvm::uvm_component_name name) : uvm::uvm_monitor(name), ap("ap") {}

    // Pre-run: Construction
    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_monitor::build_phase(phase);
        UVM_INFO("MON", "Building monitor...", uvm::UVM_LOW);
    }

    // Run-time: Execution (consumes time)
    void run_phase(uvm::uvm_phase& phase) override {
        // Objections control when the simulation finishes
        phase.raise_objection(this);

        for(int i = 0; i < 3; i++) {
            // Wait for simulated time to pass
            sc_core::wait(10, sc_core::SC_NS);
            UVM_INFO("MON", "Sampling bus...", uvm::UVM_LOW);

            // Broadcast dummy transaction
            my_transaction tx;
            ap.write(tx);
        }

        phase.drop_objection(this);
    }

    // Post-run: Cleanup
    void report_phase(uvm::uvm_phase& phase) override {
        UVM_INFO("MON", "Simulation finished successfully.", uvm::UVM_LOW);
    }
};

class my_test : public uvm::uvm_test {
public:
    my_monitor* mon;
    UVM_COMPONENT_UTILS(my_test);

    my_test(uvm::uvm_component_name name) : uvm::uvm_test(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_test::build_phase(phase);
        mon = my_monitor::type_id::create("mon", this);
    }
};

int sc_main(int argc, char* argv[]) {
    uvm::run_test("my_test");
    return 0;
}
```

## Controlling the Run Phase: Objections

Because `run_phase` executes concurrently across many components, UVM needs a way to know when the test is "done." If it waited for all `run_phase` threads to exit, simulations might run forever (due to infinite `while(true)` loops in monitors and drivers).

### The C++ Objection Implementation

In the Accellera implementation, `uvm_objection` acts as a distributed reference counter.
1. When you call `phase.raise_objection(this)`, the global objection counter increments.
2. The `uvm_phase` state machine is blocked from transitioning out of the `run_phase` as long as `m_objection_count > 0`.
3. When `phase.drop_objection(this)` is called and the counter hits zero, it triggers a system-wide `dropped()` callback. The phase state machine then automatically kills the `sc_spawn`'d threads and transitions into the `extract_phase`.

UVM solves this with **Objections**.
*   **`raise_objection()`**: "I am busy executing the test, do not end the simulation."
*   **`drop_objection()`**: "I am done with my part of the test."

The `run_phase` (and the entire simulation) ends when all raised objections have been dropped. This logic is typically handled in the `uvm_test` or inside a `uvm_sequence`.

By standardizing when things happen (phases) and how we agree to finish (objections), UVM creates highly deterministic and predictable testbenches out of independent, modular components.

## Accellera Source Implementation: `uvm_phase::execute`

How does the UVM phasing engine actually run the phases? In `src/uvmsc/phasing/uvm_phase.cpp`, the `execute()` method is the heart of the state machine.

```cpp
// Abstract representation of uvm_phase::execute
void uvm_phase::execute(uvm_component* comp) {
    // 1. Wait for phase to start
    m_phase_ready.wait();

    // 2. Call the user's virtual method
    if(this->get_name() == "build") {
        comp->build_phase(this);
    } else if(this->get_name() == "run") {
        // Run phase is a task (SC_THREAD)
        sc_core::sc_spawn(sc_bind(&uvm_component::run_phase, comp, this));
    }

    // 3. Wait for objections to drop before ending
    uvm_objection* obj = this->get_objection();
    obj->wait_for_zero();

    // 4. Transition to next phase
    m_phase_done.notify();
}
```
Notice that the `run_phase` explicitly uses `sc_spawn` to fork an independent OS thread in the SystemC kernel, which is why it consumes simulation time, unlike the bottom-up function phases.

## Lesson 76: UVM Reporting Facility

Canonical lesson: https://www.learn-systemc.com/tutorials/073-uvm-reporting-facility

Master the UVM messaging system. Learn how to log information, warnings, errors, and filter messages based on verbosity.

## How to Read This Lesson

SystemC provides standard `std::cout` and the native `sc_report_handler` for printing messages. However, when working in a large verification environment, you need much more control. You need to be able to filter messages by severity, origin, or verbosity, and you might want errors to automatically terminate the simulation.

The **UVM Reporting Facility** provides a consistent, configurable message-reporting mechanism as defined in the IEEE 1800.2 standard.

### Under the Hood: C++ Implementation in Accellera UVM-SystemC

While SystemC uses `sc_report_handler`, UVM uses a more distributed architecture. If you look at the Accellera `uvm-systemc` source code, reporting is handled by a triad of classes:
1. **`uvm_report_object`:** As discussed previously, `uvm_component` inherits from this. It provides the front-end API (`uvm_report_info()`, macro `UVM_INFO`).
2. **`uvm_report_handler`:** Each `uvm_report_object` owns a `uvm_report_handler`. The handler is responsible for storing the verbosity settings, severity overrides, and action overrides specific to that component.
3. **`uvm_report_server`:** This is a global singleton accessible via the `uvm_coreservice_t`. Once a handler decides a message should be printed, it delegates the actual string formatting and printing (to `std::cout` or a file) to the central `uvm_report_server`.

When you call `UVM_INFO`, the macro expands to check the component's `uvm_report_handler` to see if the message verbosity passes the threshold. If it does, it dynamically allocates a `uvm_report_message` object, populates it with filename, line number, and context, and sends it to the server.

## Standard and source context

## UVM Severities

Every message in UVM is assigned a severity. The severity indicates the nature of the message and dictates the default action the simulator will take:

1.  **`UVM_INFO`**: Informative message. Does not affect simulation execution.
2.  **`UVM_WARNING`**: Indicates a potential problem. Simulation continues, but the warning counter is incremented.
3.  **`UVM_ERROR`**: Indicates a real problem. Simulation continues, but the error counter is incremented. If the error count reaches a maximum threshold (`max_quit_count`), the simulation aborts.
4.  **`UVM_FATAL`**: Indicates a critical problem from which the simulation cannot recover. The simulation is terminated immediately (`UVM_EXIT`).

## UVM Verbosity

While Severities describe *what* the message is, **Verbosity** describes *how important* it is to print. Verbosity levels allow you to leave thousands of debug prints in your code but only print them when you explicitly ask for them.

The standard verbosity levels are:
*   `UVM_NONE` (always printed)
*   `UVM_LOW`
*   `UVM_MEDIUM` (default)
*   `UVM_HIGH`
*   `UVM_FULL`

When you issue a message, you assign it a verbosity. If that verbosity is less than or equal to the configured verbosity of the component (or the global `uvm_top`), the message is printed.

## Complete Reporting Example

Below is a complete, compilable `sc_main` example that demonstrates reporting macros, modifying verbosity, changing default actions, setting quit counts, and using a report catcher to demote errors.

```cpp
#include <systemc>
#include <uvm>

// 1. Define a Report Catcher to intercept and modify messages
// In the C++ implementation, uvm_report_catcher uses the visitor pattern.
// Before the uvm_report_server prints, it visits all registered catchers.
class my_catcher : public uvm::uvm_report_catcher {
public:
    action_e do_catch() override {
        // If we see an expected error, demote it to an INFO message
        if (get_severity() == uvm::UVM_ERROR && get_id() == "EXPECTED_ERR") {
            set_severity(uvm::UVM_INFO);
            set_message("Error was expected and demoted: " + get_message());
        }
        // Always return THROW to let the message proceed to the output
        return THROW;
    }
};

class my_test : public uvm::uvm_test {
public:
    UVM_COMPONENT_UTILS(my_test);

    my_test(uvm::uvm_component_name name) : uvm::uvm_test(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_test::build_phase(phase);

        // Change verbosity for this component and its children
        this->set_report_verbosity_level_hier(uvm::UVM_HIGH);

        // Force the simulation to exit on ANY warning from this component
        // (Commented out so we can see the rest of the simulation)
        // this->set_report_severity_action(uvm::UVM_WARNING, uvm::UVM_DISPLAY | uvm::UVM_EXIT);

        // Abort simulation after 5 UVM_ERRORs
        uvm::uvm_coreservice_t* cs = uvm::uvm_coreservice_t::get();
        uvm::uvm_report_server* svr = cs->get_report_server();
        svr->set_max_quit_count(5);
    }

    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);

        // Standard Info messages with varying verbosities
        UVM_INFO("TEST_LOW", "This is a UVM_LOW message.", uvm::UVM_LOW);
        UVM_INFO("TEST_HIGH", "This is a UVM_HIGH message. It prints because we changed the verbosity level.", uvm::UVM_HIGH);

        // This will not print, because UVM_FULL > UVM_HIGH
        UVM_INFO("TEST_FULL", "This is a UVM_FULL message and will be hidden.", uvm::UVM_FULL);

        // Warning
        UVM_WARNING("TEST_WARN", "Timeout value is suspiciously low.");

        // Error that gets caught and demoted by our catcher
        UVM_ERROR("EXPECTED_ERR", "This payload mismatch was expected in this test!");

        // Real Error (increments error count)
        UVM_ERROR("REAL_ERR", "An actual error occurred.");

        phase.drop_objection(this);
    }
};

int sc_main(int argc, char* argv[]) {
    // Register the custom report catcher globally
    my_catcher* catcher = new my_catcher();
    uvm::uvm_report_cb::add(nullptr, catcher);

    uvm::run_test("my_test");
    return 0;
}
```

Using UVM's reporting mechanism guarantees that your testbench logs are consistent, easily parsable by external scripts, and highly configurable at runtime without recompiling code.

## Lesson 77: TLM Communication in UVM

Canonical lesson: https://www.learn-systemc.com/tutorials/074-tlm-communication-in-uvm

Understand how UVM components communicate transaction objects using Transaction Level Modeling (TLM) ports and exports.

## How to Read This Lesson

A core philosophy of UVM is that verification components should be modular and highly reusable. If a driver accesses a monitor's variables directly, they become tightly coupled. To enforce loose coupling, UVM relies entirely on **Transaction Level Modeling (TLM)** for inter-component communication.

UVM-SystemC builds its TLM capabilities directly on top of the IEEE 1666 SystemC TLM standard. The IEEE 1800.2 UVM standard outlines three primary types of TLM interfaces used in verification: unidirectional point-to-point (put/get), bidirectional point-to-point (transport/req-rsp), and broadcast (analysis).

### Under the Hood: C++ Implementation in Accellera UVM-SystemC

How does UVM-SystemC implement these ports? By tightly integrating with the standard SystemC TLM-1.0 and TLM-2.0 core libraries.

1. **`uvm_port_base`:** All UVM ports inherit from `uvm_port_base`. This base class handles the UVM-specific connection resolution during the `end_of_elaboration_phase`. It ensures that every port is ultimately connected to an export that implements the required interface.
2. **SystemC Interface Binding:** A `uvm_blocking_put_port<T>` is effectively a wrapper around `sc_core::sc_port<tlm::tlm_blocking_put_if<T>>`. When you connect a port to an export, the UVM framework dynamically resolves the connection down to standard SystemC `bind()` calls.
3. **`uvm_analysis_port<T>` Implementation:** If you inspect the `uvm_analysis_port` source, you'll see it is incredibly lightweight. It contains an internal `std::vector` of connected `uvm_analysis_export` pointers. When you call `ap.write(tx)`, the C++ implementation simply executes a `for` loop over this vector, synchronously calling `export->write(tx)` for every connected subscriber. Because `write()` is a `void` function, it must execute in zero simulation time.

## Standard and source context

## TLM-1.0 Blocking Ports

In UVM, components pass data using `uvm_sequence_item` or `uvm_transaction` objects.
To send data from one component to another, we use ports and exports.

*   **Port (`uvm_blocking_put_port`)**: The initiator of the communication. It requires an implementation of a method (like `put()`) to exist.
*   **Export (`uvm_blocking_put_export` / `uvm_port_base`)**: The provider of the communication. It supplies the implementation.

In SystemC, the class supplying the export typically implements the interface methods (e.g. `tlm::tlm_blocking_put_if`) and binds the export to `*this`.

## Analysis Ports (Broadcast Communication)

Blocking `put` and `get` ports are strictly one-to-one point-to-point connections. But what if a Monitor sees a transaction on the bus and needs to send it to a Scoreboard, a Coverage Collector, and a Protocol Checker all at once?

For this, UVM uses **Analysis Ports**. Analysis ports implement a broadcast (publish/subscribe) mechanism.
*   An Analysis Port can be bound to zero, one, or many Analysis Exports.
*   The transaction is passed by calling `write()`.
*   `write()` is strictly a `void` non-blocking function. It must execute in zero time.

### UVM Analysis Ports in SystemC

UVM-SystemC provides convenient wrappers around SystemC's `tlm_analysis_port`:
*   `uvm_analysis_port<T>`
*   `uvm_analysis_export<T>`
*   `uvm_analysis_imp<T, IMP>`

On the receiving end, you typically use a `uvm_subscriber`. The `uvm_subscriber` base class automatically provides an `analysis_export` and requires you to implement the `write()` function.

## Sequencer-Driver Communication

The most specialized TLM connection in UVM is the communication between a `uvm_sequencer` and a `uvm_driver`.

UVM defines a dedicated request-response channel (`uvm_tlm_req_rsp_channel`) and specialized ports (`uvm_seq_item_pull_port` and `uvm_seq_item_pull_export`) for this. The driver acts as the active "puller", requesting items from the sequencer using methods like `get_next_item()` and `item_done()`.

## Complete TLM Example

Below is a complete, fully compilable `sc_main` example demonstrating a one-to-one point-to-point connection and a broadcast analysis connection.

```cpp
#include <systemc>
#include <uvm>

// The data object exchanged between components
class my_transaction : public uvm::uvm_transaction {
public:
    int data;
    UVM_OBJECT_UTILS(my_transaction);
    my_transaction(const std::string& name = "my_transaction") : uvm::uvm_transaction(name), data(0) {}
};

// 1. Producer: Initiates a transaction via a blocking put port
class producer : public uvm::uvm_component {
public:
    UVM_COMPONENT_UTILS(producer);

    uvm::uvm_blocking_put_port<my_transaction> put_port;

    producer(uvm::uvm_component_name name)
        : uvm::uvm_component(name), put_port("put_port") {}

    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);
        my_transaction tx;
        tx.data = 42;

        UVM_INFO("PROD", "Sending transaction data: 42", uvm::UVM_LOW);

        // This is a blocking call; it will wait until the consumer finishes processing
        put_port.put(tx);

        phase.drop_objection(this);
    }
};

// 2. Consumer/Monitor: Receives point-to-point and Broadcasts via Analysis Port
class consumer : public uvm::uvm_component,
                 public tlm::tlm_blocking_put_if<my_transaction> {
public:
    UVM_COMPONENT_UTILS(consumer);

    // TLM-1 export for receiving point-to-point data
    sc_core::sc_export<tlm::tlm_blocking_put_if<my_transaction>> put_export;

    // Analysis port for broadcasting data
    uvm::uvm_analysis_port<my_transaction> ap;

    consumer(uvm::uvm_component_name name)
        : uvm::uvm_component(name), put_export("put_export"), ap("ap") {

        // Bind the export to 'this' component
        put_export.bind(*this);
    }

    // Implementation of the blocking put method
    void put(const my_transaction& tx) override {
        UVM_INFO("CONS", "Received transaction, consuming time...", uvm::UVM_LOW);
        sc_core::wait(10, sc_core::SC_NS); // Consume simulation time

        UVM_INFO("CONS", "Broadcasting transaction to subscribers", uvm::UVM_LOW);
        // Non-blocking broadcast
        ap.write(tx);
    }
};

// 3. Subscriber (Scoreboard): Listens to Analysis Broadcasts
class scoreboard : public uvm::uvm_subscriber<my_transaction> {
public:
    UVM_COMPONENT_UTILS(scoreboard);

    scoreboard(uvm::uvm_component_name name) : uvm::uvm_subscriber<my_transaction>(name) {}

    // Implement the write() function mandated by uvm_subscriber
    void write(const my_transaction& t) override {
        UVM_INFO("SB", "Scoreboard intercepted broadcast transaction!", uvm::UVM_LOW);
        if (t.data == 42) {
            UVM_INFO("SB", "Data matches expected value.", uvm::UVM_LOW);
        }
    }
};

// 4. Environment: Connects them all
class my_env : public uvm::uvm_env {
public:
    producer* prod;
    consumer* cons;
    scoreboard* sb;

    UVM_COMPONENT_UTILS(my_env);

    my_env(uvm::uvm_component_name name) : uvm::uvm_env(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_env::build_phase(phase);
        prod = producer::type_id::create("prod", this);
        cons = consumer::type_id::create("cons", this);
        sb   = scoreboard::type_id::create("sb", this);
    }

    void connect_phase(uvm::uvm_phase& phase) override {
        // Point-to-point: Port binds to Export
        prod->put_port.connect(cons->put_export);

        // Broadcast: Analysis Port binds to Analysis Export (provided by uvm_subscriber)
        cons->ap.connect(sb->analysis_export);
    }
};

class my_test : public uvm::uvm_test {
public:
    my_env* env;
    UVM_COMPONENT_UTILS(my_test);

    my_test(uvm::uvm_component_name name) : uvm::uvm_test(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_test::build_phase(phase);
        env = my_env::type_id::create("env", this);
    }
};

int sc_main(int argc, char* argv[]) {
    uvm::run_test("my_test");
    return 0;
}
```

By heavily leveraging TLM, UVM components remain oblivious to what they are connected to, guaranteeing that your VIP (Verification Intellectual Property) can be reused cleanly across different projects.

## Lesson 78: The UVM Configuration Database (uvm_config_db)

Canonical lesson: https://www.learn-systemc.com/tutorials/075-the-uvm-configuration-database-uvm-config-db

Discover how to pass virtual interfaces, integers, and objects across your UVM hierarchy without hardcoding paths using the uvm_config_db.

## How to Read This Lesson

In a large verification environment, passing configuration parameters down a deep component hierarchy can be extremely tedious. If a monitor deeply nested inside an agent needs to know whether the bus is configured for 32-bit or 64-bit mode, passing that boolean down through the test, the environment, the agent, and finally the monitor requires boilerplate code at every level.

Worse, what if you need to pass a handle to the physical pins (a virtual interface or a SystemC signal pointer)?

The **UVM Configuration Database** (`uvm_config_db`) solves this. It acts as a central repository where any component can store a variable (a `set`), and any component can retrieve that variable (a `get`), provided they know the correct hierarchy path and variable name. According to the IEEE 1800.2 standard, the database utilizes type-safe parameterized classes built upon a generic resource pool.

### Under the Hood: C++ Implementation in Accellera UVM-SystemC

To appreciate the `uvm_config_db`, we must look at how the `uvm-systemc` repository implements it:

1. **`uvm_resource_pool`:** The config database is actually a type-safe wrapper around a global singleton called the `uvm_resource_pool`. The resource pool stores a collection of type-erased `uvm_resource_base` objects.
2. **Type-Safety via Templates:** `uvm_config_db<T>` is a template class. When you call `set<int>()`, the compiler instantiates a specific version of the database. This guarantees that if a component performs a `set<int>("var")`, and another component performs a `get<bool>("var")`, the cast fails safely and returns false, preventing catastrophic memory errors.
3. **Regular Expressions and Caching:** The hierarchical strings (like `"env.*.driver"`) you provide to `set()` are translated into POSIX regular expressions. Because compiling regex during simulation is slow, UVM-SystemC heavily caches these lookups inside the `uvm_resource_pool` to ensure fast retrieval during the `build_phase`.

## Standard and source context

## How `uvm_config_db` works

The `uvm_config_db` is a parameterized class. You must specify the *type* of the data you are storing or retrieving.

The two primary static methods are `set()` and `get()`:

```cpp
static void set(uvm_component* cntxt, const std::string& inst_name, const std::string& field_name, const T& value);

static bool get(uvm_component* cntxt, const std::string& inst_name, const std::string& field_name, T& value);
```

*   **`cntxt`**: The starting point in the UVM hierarchy (usually `this`). If setting globally, use `uvm_top` (or `nullptr`).
*   **`inst_name`**: The relative hierarchical path to the component(s) that should receive this configuration. Wildcards (`*`) are heavily used here.
*   **`field_name`**: The string identifier for the variable you are storing.
*   **`value`**: The actual data being stored (for `set`) or the variable to populate (for `get`).

## Complete Configuration Example

Below is a complete, fully compilable `sc_main` program demonstrating how to pass a physical interface pointer (virtual interface) and a configuration integer from the top-level test bench down to a nested driver component using the `uvm_config_db`.

```cpp
#include <systemc>
#include <uvm>

// A dummy transaction for the driver
class my_transaction : public uvm::uvm_transaction {
public:
    UVM_OBJECT_UTILS(my_transaction);
    my_transaction(const std::string& name = "my_transaction") : uvm::uvm_transaction(name) {}
};

// A dummy physical interface wrapper
class my_bus_if {
public:
    sc_core::sc_signal<bool> clk;
    my_bus_if(const char* name) : clk(name) {}
};

// 1. The Driver retrieves the configuration
class my_driver : public uvm::uvm_driver<my_transaction> {
public:
    UVM_COMPONENT_UTILS(my_driver);

    my_bus_if* vif;
    int is_active_;

    my_driver(uvm::uvm_component_name name) : uvm::uvm_driver<my_transaction>(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_driver<my_transaction>::build_phase(phase);

        // Retrieve the virtual interface
        if (!uvm::uvm_config_db<my_bus_if*>::get(this, "", "vif", vif)) {
            UVM_FATAL("DRV/NOVIF", "No virtual interface specified for this driver instance");
        }

        // Retrieve the active flag (default to active if not found)
        if (!uvm::uvm_config_db<int>::get(this, "", "is_active", is_active_)) {
            is_active_ = 1;
        }
    }

    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);
        if (is_active_) {
            UVM_INFO("DRV", "Driver is active, driving virtual interface pins.", uvm::UVM_LOW);
            vif->clk.write(true);
        } else {
            UVM_INFO("DRV", "Driver is passive.", uvm::UVM_LOW);
        }
        phase.drop_objection(this);
    }
};

// 2. The Agent instantiates the driver
class my_agent : public uvm::uvm_agent {
public:
    my_driver* driver;
    UVM_COMPONENT_UTILS(my_agent);

    my_agent(uvm::uvm_component_name name) : uvm::uvm_agent(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_agent::build_phase(phase);
        driver = my_driver::type_id::create("driver", this);
    }
};

// 3. The Environment instantiates the agent
class my_env : public uvm::uvm_env {
public:
    my_agent* agent;
    UVM_COMPONENT_UTILS(my_env);

    my_env(uvm::uvm_component_name name) : uvm::uvm_env(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_env::build_phase(phase);
        agent = my_agent::type_id::create("agent", this);
    }
};

// 4. The Test sets configurations for the descendants
class my_test : public uvm::uvm_test {
public:
    my_env* env;
    UVM_COMPONENT_UTILS(my_test);

    my_test(uvm::uvm_component_name name) : uvm::uvm_test(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_test::build_phase(phase);

        // Target: "env.agent.driver" inside this context
        uvm::uvm_config_db<int>::set(this, "env.agent.driver", "is_active", 0);
        // Notice we are turning the driver off (passive) for this specific test

        env = my_env::type_id::create("env", this);
    }
};

// 5. The Top-Level sets the virtual interface globally
int sc_main(int argc, char* argv[]) {
    // Instantiate the physical bus interface
    my_bus_if bus("bus");

    // Pass the pointer to the bus into the UVM configuration database
    // We use a global context (nullptr) and target all instances ("*")
    uvm::uvm_config_db<my_bus_if*>::set(nullptr, "*", "vif", &bus);

    // Start the UVM test
    uvm::run_test("my_test");
    return 0;
}
```

## Best Practices

1.  **Do it in `build_phase`**: Setting and getting configurations should generally happen during the `build_phase`. If you wait until `connect_phase` or `run_phase`, children may have already been constructed with incorrect defaults.
2.  **Check the return value of `get()`**: Always verify that `get()` returns true, and provide a sensible default (or issue a `UVM_FATAL`) if it returns false.
3.  **Minimize wildcards**: While setting a configuration to `"*"` is easy, it can cause performance issues in massive testbenches and makes it hard to track who is configuring what. Be as specific as possible with the `inst_name` argument (e.g., `"env.agent.driver"`).
4.  **Configuration Objects**: Instead of passing dozens of integers and booleans individually, wrap them in a configuration class deriving from `uvm_object`, and pass a pointer to that object via the database.

## Lesson 79: UVM-SystemC Bridge: Verification Architecture

Canonical lesson: https://www.learn-systemc.com/tutorials/076-uvm-systemc-bridge-verification-architecture

How UVM-SystemC layers standard Universal Verification Methodology on top of the SystemC simulation kernel.

## How to Read This Lesson

# UVM-SystemC Bridge: Verification Architecture

UVM-SystemC brings the Universal Verification Methodology (UVM)â€”the gold standard for hardware verification in SystemVerilogâ€”directly into the C++ SystemC ecosystem.

While SystemC provides the fundamental discrete-event simulation kernel and modeling paradigms (like TLM), UVM-SystemC provides the **verification structure**. It introduces standardized architectures for testbenches, phasing, configuration, and reporting, ensuring that verification environments are highly reusable and predictable.

## Standard and source context

## The UVM-SystemC Architecture

According to the Accellera UVM-SystemC Language Reference Manual, the methodology is built on a few core pillars:

1. **`uvm_object`**: The base class for dynamic, transient data (like transactions and sequences).
2. **`uvm_component`**: The base class for structural, permanent hierarchy (like drivers, monitors, and scoreboards).
3. **Phasing**: A standardized execution flow (`build_phase`, `connect_phase`, `run_phase`, etc.) that coordinates the entire testbench.
4. **The Factory**: A mechanism allowing types to be overridden at runtime, enabling tests to replace generic transactions or components with specialized ones without touching the original source code.
5. **Configuration DB**: A centralized database (`uvm_config_db`) for passing parameters and interface handles down the component hierarchy.

## Typical Component Hierarchy

A UVM-SystemC environment strictly separates the Design Under Test (DUT) from the verification logic. The testbench hierarchy typically looks like this:

- **`uvm_test`**: The top-level block. It configures the environment and starts the stimulus (sequences).
  - **`uvm_env`**: The environment grouping agents and scoreboards.
    - **`uvm_agent`**: A reusable block encapsulating a specific protocol (e.g., AXI, UART).
      - **`uvm_sequencer`**: Arbitrates and feeds transactions to the driver.
      - **`uvm_driver`**: Translates transactions into pin wiggles or TLM calls.
      - **`uvm_monitor`**: Observes the bus and publishes observed transactions.
    - **`uvm_scoreboard`**: Compares observed transactions against expected behavior.

## Complete Example: The Top-Level UVM Architecture

The following complete, compilable example demonstrates the absolute baseline architecture of a UVM-SystemC simulation. It defines a component hierarchy, implements the standard UVM phases, and uses `uvm::run_test()` to bootstrap the verification environment inside `sc_main`.

```cpp
#include <systemc>
#include <uvm>

// 1. A dummy Driver Component
class my_driver : public uvm::uvm_driver<uvm::uvm_sequence_item> {
public:
    UVM_COMPONENT_UTILS(my_driver); // Register with the UVM factory

    my_driver(uvm::uvm_component_name name) : uvm::uvm_driver<uvm::uvm_sequence_item>(name) {}

    // The run_phase consumes time and drives the simulation
    void run_phase(uvm::uvm_phase& phase) override {
        UVM_INFO("DRIVER", "Driver is starting execution...", uvm::UVM_LOW);

        // Raise an objection to prevent the simulation from ending
        phase.raise_objection(this);

        sc_core::wait(100, sc_core::SC_NS);
        UVM_INFO("DRIVER", "Driving transaction 1...", uvm::UVM_LOW);

        sc_core::wait(100, sc_core::SC_NS);
        UVM_INFO("DRIVER", "Driving transaction 2...", uvm::UVM_LOW);

        // Drop objection to allow simulation to finish
        phase.drop_objection(this);
    }
};

// 2. An Agent grouping the driver
class my_agent : public uvm::uvm_agent {
public:
    UVM_COMPONENT_UTILS(my_agent);

    my_driver* driver;

    my_agent(uvm::uvm_component_name name) : uvm::uvm_agent(name), driver(nullptr) {}

    // The build_phase constructs children top-down
    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_agent::build_phase(phase);
        UVM_INFO("AGENT", "Building driver...", uvm::UVM_MEDIUM);

        // Create the driver using the UVM Factory
        driver = my_driver::type_id::create("driver", this);
    }
};

// 3. The Environment grouping agents and scoreboards
class my_env : public uvm::uvm_env {
public:
    UVM_COMPONENT_UTILS(my_env);

    my_agent* agent;

    my_env(uvm::uvm_component_name name) : uvm::uvm_env(name), agent(nullptr) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_env::build_phase(phase);
        UVM_INFO("ENV", "Building agent...", uvm::UVM_MEDIUM);
        agent = my_agent::type_id::create("agent", this);
    }
};

// 4. The Top-Level Test
class my_test : public uvm::uvm_test {
public:
    UVM_COMPONENT_UTILS(my_test);

    my_env* env;

    my_test(uvm::uvm_component_name name) : uvm::uvm_test(name), env(nullptr) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_test::build_phase(phase);
        UVM_INFO("TEST", "Building environment...", uvm::UVM_MEDIUM);
        env = my_env::type_id::create("env", this);
    }

    // Check phase runs after simulation finishes
    void check_phase(uvm::uvm_phase& phase) override {
        UVM_INFO("TEST", "Performing final end-of-test checks.", uvm::UVM_LOW);
    }
};

// 5. The sc_main Entry Point
int sc_main(int argc, char* argv[]) {
    // Instead of explicitly instantiating components and calling sc_start(),
    // UVM-SystemC takes control of the simulation execution.

    // run_test() automatically creates the test component specified by
    // the +UVM_TESTNAME command-line argument (or passed dynamically).
    // It then automatically executes the UVM phases and invokes sc_start() internally.
    uvm::run_test("my_test");

    return 0;
}
```

### Key Differences from Pure SystemC

1. **Instantiation**: You do not use standard C++ `new` or `SC_MODULE` constructors for the hierarchy. Instead, components are instantiated in the `build_phase` using the factory (`type_id::create`). This is what makes UVM reusable.
2. **Execution Control**: `sc_start()` is completely hidden. `uvm::run_test()` manages the SystemC kernel for you.
3. **Objections**: The simulation stops automatically when all `run_phase` objections are dropped, rather than running indefinitely or stopping via a hardcoded time limit.

## Under the Hood: `run_test` and `UVM_COMPONENT_UTILS`

While UVM-SystemC abstractly mirrors SystemVerilog UVM, it heavily relies on C++ metaprogramming to bridge into the SystemC kernel.

When you call `uvm::run_test("my_test")`, the UVM-SystemC library fetches the singleton `uvm_root::get()`. `uvm_root` is a specialized `uvm_component` that serves as the invisible top-level module (the parent of `my_test`). Inside `run_test()`, the core library queries the UVM factory to instantiate the test string provided. It then manually invokes `sc_core::sc_start()` internally. During the `run_phase`, UVM-SystemC spawns threads (`SC_THREAD`) for every component's `run_phase()` method. When the global objection count hits zero, `uvm_root` calls `sc_core::sc_stop()`.

The magic of the factory is implemented through the `UVM_COMPONENT_UTILS(T)` macro. In the `uvm-systemc` repository, this macro expands to declare a nested `type_id` struct and a static factory registration proxy. At C++ static initialization time (before `main` even starts), this proxy object registers a string-to-creator mapping in the global `uvm_factory`. This is why you can call `type_id::create("name")` and receive a dynamically allocated polymorphic instance without ever hardcoding the `new` keyword.

In the next tutorial, we will explore the `uvm_object`, the factory, and how to write reusable transaction data structures.

## Lesson 80: UVM-SystemC Bridge: Objects, Factory, and Policy Classes

Canonical lesson: https://www.learn-systemc.com/tutorials/077-uvm-systemc-bridge-objects-factory-and-policy-clas

uvm_object, copy/compare/print hooks, factory creation, and reusable transaction design.

## How to Read This Lesson

# UVM-SystemC Bridge: Objects, Factory, and Policy Classes

While `uvm_component` forms the static structural hierarchy of your testbench, `uvm_object` is the fundamental base class for all dynamic, transient data in UVM-SystemC.

Transactions, sequences, and configuration objects all inherit from `uvm_object`.

## Standard and source context

## Transaction Objects (Sequence Items)

A transaction object describes an atomic operation (e.g., a bus read, an ethernet packet, a register write). In UVM, transactions inherit from `uvm_sequence_item` (which itself inherits from `uvm_object`).

To ensure a transaction is fully reusable across scoreboards, drivers, and monitors, it must implement **Policy Hooks**.

## Policy Hooks: Print, Copy, Compare, Pack

UVM-SystemC objects provide standard virtual methods that you are expected to override:

- `do_print(uvm_printer&)`: How the object represents itself as text.
- `do_copy(const uvm_object&)`: How to deep-copy the object.
- `do_compare(const uvm_object&, uvm_comparer&)`: How to check if two objects are equivalent.
- `do_pack(uvm_packer&)` / `do_unpack(uvm_packer&)`: How to serialize the object to/from raw bit arrays.

By implementing these hooks, generic UVM components (like scoreboards) can compare transactions without knowing their specific underlying types.

## The UVM Factory

The UVM Factory is a design pattern that allows you to substitute one object type for another dynamically at runtime.

If a testbench instantiates `bus_transaction` via the factory, a specific test can instruct the factory to substitute `error_bus_transaction` instead. The environment will automatically use the new type without a single line of the environment's source code being changed.

## Complete Example: Transactions, Policies, and Factory Overrides

Here is a complete `sc_main` example demonstrates how to write a fully compliant `uvm_sequence_item`, override its policy hooks, register it with the factory, and dynamically override it from a test.

```cpp
#include <systemc>
#include <uvm>
#include <string>

// 1. A Baseline Transaction Object
class bus_item : public uvm::uvm_sequence_item {
public:
    // Register with the UVM object factory
    UVM_OBJECT_UTILS(bus_item);

    sc_dt::uint64 address;
    sc_dt::uint32 data;
    bool is_write;

    // Default constructor is required by the factory
    bus_item(const std::string& name = "bus_item")
        : uvm::uvm_sequence_item(name), address(0), data(0), is_write(false) {}

    // 2. Implement the Print hook
    void do_print(uvm::uvm_printer& printer) const override {
        uvm::uvm_sequence_item::do_print(printer);
        printer.print_field_int("address", address, 64, uvm::UVM_HEX);
        printer.print_field_int("data", data, 32, uvm::UVM_HEX);
        printer.print_field_int("is_write", is_write, 1, uvm::UVM_BIN);
    }

    // 3. Implement the Copy hook
    void do_copy(const uvm::uvm_object& rhs) override {
        const bus_item* rhs_cast = dynamic_cast<const bus_item*>(&rhs);
        if (rhs_cast == nullptr) {
            UVM_FATAL("COPY", "Cast failed in do_copy");
        }
        uvm::uvm_sequence_item::do_copy(rhs);
        this->address = rhs_cast->address;
        this->data = rhs_cast->data;
        this->is_write = rhs_cast->is_write;
    }

    // 4. Implement the Compare hook
    bool do_compare(const uvm::uvm_object& rhs, uvm::uvm_comparer* comparer) const override {
        const bus_item* rhs_cast = dynamic_cast<const bus_item*>(&rhs);
        if (rhs_cast == nullptr) return false;

        bool match = uvm::uvm_sequence_item::do_compare(rhs, comparer);
        match &= comparer->compare_field_int("address", this->address, rhs_cast->address, 64);
        match &= comparer->compare_field_int("data", this->data, rhs_cast->data, 32);
        match &= comparer->compare_field_int("is_write", this->is_write, rhs_cast->is_write, 1);
        return match;
    }
};

// 5. An Error-Injection Subclass
class error_bus_item : public bus_item {
public:
    UVM_OBJECT_UTILS(error_bus_item);

    bool force_error;

    error_bus_item(const std::string& name = "error_bus_item")
        : bus_item(name), force_error(true) {}

    void do_print(uvm::uvm_printer& printer) const override {
        bus_item::do_print(printer); // Print parent fields
        printer.print_field_int("force_error", force_error, 1, uvm::UVM_BIN);
    }
};

// 6. A Component that uses the transaction
class generic_driver : public uvm::uvm_component {
public:
    UVM_COMPONENT_UTILS(generic_driver);

    generic_driver(uvm::uvm_component_name name) : uvm::uvm_component(name) {}

    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);

        // We ask the factory to create a "bus_item".
        // If an override is set, we might get an "error_bus_item" instead!
        bus_item* item = bus_item::type_id::create("item");

        UVM_INFO("DRIVER", "Driver created transaction:", uvm::UVM_LOW);
        item->print(); // Uses do_print() under the hood

        phase.drop_objection(this);
    }
};

// 7. The Top-Level Test
class factory_test : public uvm::uvm_test {
public:
    UVM_COMPONENT_UTILS(factory_test);

    generic_driver* driver;

    factory_test(uvm::uvm_component_name name) : uvm::uvm_test(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_test::build_phase(phase);

        // 8. Factory Override:
        // Instruct the factory: Anytime someone asks to create a "bus_item",
        // give them an "error_bus_item" instead.
        set_type_override("bus_item", "error_bus_item");

        driver = generic_driver::type_id::create("driver", this);
    }
};

int sc_main(int argc, char* argv[]) {
    // Run the test. The driver will ask for a bus_item, but will receive
    // an error_bus_item due to the factory override in build_phase.
    uvm::run_test("factory_test");
    return 0;
}
```

## Under the Hood: The Factory and Dynamic Casting

UVM-SystemC implements the factory pattern via a central singleton called `uvm_default_factory`.

When `set_type_override` (or `set_type_override_by_type`) is called, the factory populates a map `std::map<uvm_object_wrapper*, uvm_object_wrapper*>` (conceptually). Later, when `bus_item::type_id::create()` executes, it consults the factory to resolve the most derived override. It then invokes the `create_object()` virtual method on the proxy wrapper to perform the C++ `new` allocation.

Furthermore, notice the mandatory use of `dynamic_cast<const bus_item*>(&rhs)` inside `do_copy` and `do_compare`. Because UVM-SystemC relies heavily on polymorphism, the framework passes generic `uvm_object&` references to policy hooks. C++ requires RTTI (Run-Time Type Information) to safely downcast the object back to the user-defined `bus_item` to access member variables like `address` and `data`.

A critical C++ implementation detail to be aware of: `uvm_object::clone()` is heavily used in verification environments (e.g., when a monitor captures a transaction). Under the hood, `clone()` executes `uvm_object* obj = this->create(); obj->copy(this);`. Therefore, failing to register your class with `UVM_OBJECT_UTILS` breaks `create()`, which catastrophically breaks `clone()`.

### Design Rules for Transactions

1. **Implement Policies:** If you don't override `do_compare`, `item_a->compare(item_b)` will only compare base class properties, silently ignoring your custom fields (like `address` or `data`), breaking your scoreboards.
2. **Use Factory Macros:** Always use `UVM_OBJECT_UTILS(class_name)` to register the object. Without this, `type_id::create` will not compile.
3. **Avoid Simulator Resources:** Transactions should not own SystemC events (`sc_event`), ports, or modules. They are pure data containers designed to be passed around, cloned, and deleted instantly.

## Lesson 81: UVM-SystemC Bridge: Components, Phasing, and Configuration

Canonical lesson: https://www.learn-systemc.com/tutorials/078-uvm-systemc-bridge-components-phasing-and-configur

uvm_component hierarchy, build/connect/run behavior, configuration/resource patterns, and test structure.

## How to Read This Lesson

# UVM-SystemC Bridge: Components, Phasing, and Configuration

While SystemC relies on `sc_core::sc_module` and `SC_CTOR` to build structural hierarchy, UVM-SystemC introduces the `uvm_component` class and an advanced, multi-stage **Phasing** mechanism to orchestrate testbenches uniformly.

## Standard and source context

## UVM Phasing

Standard SystemC has a limited elaboration phase followed directly by simulation (`sc_start`).
UVM-SystemC expands this significantly to enforce a standard lifecycle for all verification components:

1. **`build_phase`**: Components instantiate their children from the top-down.
2. **`connect_phase`**: Components connect their TLM ports, exports, and interfaces from the bottom-up.
3. **`end_of_elaboration_phase`**: Final topology checks before simulation starts.
4. **`start_of_simulation_phase`**: Printing topologies, configuring banners.
5. **`run_phase`**: The only time-consuming phase. Stimulus generation and signal driving happen here.
6. **`extract_phase`**: Data is gathered from scoreboards and coverage collectors.
7. **`check_phase`**: Final pass/fail assertions are evaluated.
8. **`report_phase`**: Test results and logs are dumped to the terminal or file.

By strictly adhering to these phases, UVM components can be plugged together reliably without initialization race conditions.

## The Configuration Database (`uvm_config_db`)

The `uvm_config_db` is a type-safe, hierarchical registry used to pass configuration data down the component tree. Instead of passing long lists of constructor arguments, a parent component (like a Test) can place a configuration object in the database, and a deeply nested child (like a Driver) can retrieve it during its `build_phase`.

## Complete Example: Phasing and `uvm_config_db`

Here is a complete example demonstrates the exact execution order of UVM phases and shows how a top-level test configures a child driver's behavior using the `uvm_config_db`.

```cpp
#include <systemc>
#include <uvm>
#include <string>

// 1. A Reusable Driver Component
class my_driver : public uvm::uvm_component {
public:
    UVM_COMPONENT_UTILS(my_driver);

    int max_transactions;

    my_driver(uvm::uvm_component_name name)
        : uvm::uvm_component(name), max_transactions(1) {} // Default is 1

    // Phase 1: Build Phase
    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_component::build_phase(phase);
        UVM_INFO("DRIVER", "build_phase executing...", uvm::UVM_LOW);

        // Attempt to retrieve 'max_transactions' from the config DB
        // The first argument 'this' provides the hierarchical path context.
        if (!uvm::uvm_config_db<int>::get(this, "", "max_transactions", max_transactions)) {
            UVM_WARNING("DRIVER", "No config found for max_transactions. Using default.");
        } else {
            UVM_INFO("DRIVER", "Config retrieved successfully!", uvm::UVM_LOW);
        }
    }

    // Phase 2: Connect Phase
    void connect_phase(uvm::uvm_phase& phase) override {
        UVM_INFO("DRIVER", "connect_phase executing...", uvm::UVM_LOW);
    }

    // Phase 5: Run Phase (Time Consuming)
    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);

        UVM_INFO("DRIVER", "run_phase started.", uvm::UVM_LOW);

        for (int i = 0; i < max_transactions; ++i) {
            sc_core::wait(10, sc_core::SC_NS);
            UVM_INFO("DRIVER", "Driving transaction...", uvm::UVM_LOW);
        }

        phase.drop_objection(this);
    }

    // Phase 7: Check Phase
    void check_phase(uvm::uvm_phase& phase) override {
        UVM_INFO("DRIVER", "check_phase executing...", uvm::UVM_LOW);
    }
};

// 2. The Top-Level Test
class config_test : public uvm::uvm_test {
public:
    UVM_COMPONENT_UTILS(config_test);

    my_driver* driver;

    config_test(uvm::uvm_component_name name) : uvm::uvm_test(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_test::build_phase(phase);

        UVM_INFO("TEST", "build_phase executing...", uvm::UVM_LOW);

        // Place a configuration value into the DB for the driver.
        // We target the exact hierarchical path of the driver ("driver").
        uvm::uvm_config_db<int>::set(this, "driver", "max_transactions", 3);

        // Instantiate the driver AFTER setting the config, so the driver
        // can retrieve it during its own build_phase.
        driver = my_driver::type_id::create("driver", this);
    }

    void report_phase(uvm::uvm_phase& phase) override {
        UVM_INFO("TEST", "report_phase: Test completed successfully.", uvm::UVM_NONE);
    }
};

// 3. Simulation Entry Point
int sc_main(int argc, char* argv[]) {
    // Start the UVM test.
    // This will automatically sequence the phases:
    // build -> connect -> end_of_elaboration -> start_of_simulation -> run -> extract -> check -> report
    uvm::run_test("config_test");
    return 0;
}
```

### Best Practices

1. **Top-Down Build:** `build_phase` executes top-down. The parent builds first, allowing it to set configurations in the `uvm_config_db` before the child's `build_phase` runs.
2. **Bottom-Up Connect:** `connect_phase` executes bottom-up. You must not attempt to send TLM transactions or use ports during `build_phase` because they are not connected yet.
3. **Objections:** The `run_phase` operates concurrently across all components in the hierarchy. The simulation only ends when all components have dropped their objections via `phase.drop_objection(this)`. Do not forget to drop your objection, or the simulation will hang forever.

## Under the Hood: The `uvm_resource_pool` and Phase Execution

While `uvm_config_db<T>::set()` feels like a simple map insertion, it is actually a wrapper around the global `uvm_resource_pool`.

When you set a value, the library creates a type-safe `uvm_resource<T>` object on the heap and inserts it into a central database. The string hierarchical paths (like `"driver"`) are compiled into POSIX regular expressions. When `uvm_config_db<T>::get()` is called, the pool iterates over the resources and attempts a regex match against the current component's absolute path (e.g., `uvm_test_top.driver`).

Because of this regex evaluation, `uvm_config_db` accesses can be extremely slow if called dynamically during `run_phase`. A major C++ performance optimization rule in UVM-SystemC is to only perform `.get()` calls during `build_phase`, caching the retrieved values locally in member variables for use during `run_phase`.

Regarding execution, `run_phase` is the only phase that actually consumes SystemC simulation time. The UVM-SystemC core uses `sc_core::sc_spawn` to spin off an individual `SC_THREAD` for the `run_phase` of every instantiated component. The phasing state machine explicitly yields to the SystemC scheduler (`sc_start()`), allowing these spawned threads to advance time. Once the global objection counter drops to zero, the `uvm_root` singleton triggers `sc_stop()` to forcibly terminate the simulation delta cycles.

## Source-reading checkpoint

For the component bridge, inspect `src/uvmsc` around `uvm_component`, phase dispatch, and configuration access. The source trail should clarify when hierarchy exists and when phases act on it.

## Lesson 82: UVM-SystemC Bridge: Sequences, Sequencers, Drivers, and TLM

Canonical lesson: https://www.learn-systemc.com/tutorials/079-uvm-systemc-bridge-sequences-sequencers-drivers-an

How stimulus flows from sequences through sequencers and drivers, and how TLM connects verification components.

## How to Read This Lesson

# UVM-SystemC Bridge: Sequences, Sequencers, Drivers, and TLM

In a robust verification environment, you must separate **what** you want to test (the scenario) from **how** you wiggle the pins (the protocol execution).

UVM-SystemC achieves this through a triad of objects:
1. **Sequences (`uvm_sequence`)**: Generate dynamic streams of transactions (the "what").
2. **Sequencers (`uvm_sequencer`)**: Arbitrate requests and pass transactions from sequences down to drivers.
3. **Drivers (`uvm_driver`)**: Pull transactions from the sequencer and convert them into physical signal wiggles or SystemC TLM-2.0 calls (the "how").

Furthermore, Monitors observe the bus and use UVM TLM Analysis Ports (`uvm_analysis_port`) to broadcast observed transactions to Scoreboards without tightly coupling them together.

## Standard and source context

## The Sequence-Driver Handshake

The communication between a sequencer and a driver uses a specialized TLM port mechanism: `seq_item_port`. The driver executes an infinite loop during the `run_phase`:
1. `seq_item_port.get_next_item(req);` // Blocks until a sequence provides a transaction.
2. Drive the physical interface.
3. `seq_item_port.item_done();` // Signals to the sequence that the transaction is complete.

## Complete Example: Sequence, Driver, and Analysis Port

The following complete, compilable `sc_main` example demonstrates a sequence generating transactions, a sequencer routing them to a driver, the driver executing them, and then broadcasting the completion to an Analysis Port (simulating a monitor).

```cpp
#include <systemc>
#include <uvm>
#include <string>

// 1. The Transaction Object
class my_item : public uvm::uvm_sequence_item {
public:
    UVM_OBJECT_UTILS(my_item);
    int data;

    my_item(const std::string& name = "my_item") : uvm::uvm_sequence_item(name), data(0) {}
};

// 2. The Sequence (Generates Stimulus)
class my_sequence : public uvm::uvm_sequence<my_item> {
public:
    UVM_OBJECT_UTILS(my_sequence);

    my_sequence(const std::string& name = "my_sequence") : uvm::uvm_sequence<my_item>(name) {}

    // The sequence body contains the scenario logic
    void body() override {
        UVM_INFO("SEQUENCE", "Starting sequence generation...", uvm::UVM_LOW);

        for (int i = 1; i <= 3; ++i) {
            // Create a new item
            my_item* req = my_item::type_id::create("req");

            start_item(req); // Wait for sequencer arbitration
            req->data = i * 10; // Randomize or configure the payload
            finish_item(req); // Send to driver and wait for item_done()

            UVM_INFO("SEQUENCE", ("Transaction " + std::to_string(i) + " completed.").c_str(), uvm::UVM_LOW);
        }
    }
};

// 3. The Driver (Executes Protocol)
class my_driver : public uvm::uvm_driver<my_item> {
public:
    UVM_COMPONENT_UTILS(my_driver);

    // An analysis port to broadcast completed transactions (simulating a monitor)
    uvm::uvm_analysis_port<my_item> ap;

    my_driver(uvm::uvm_component_name name) : uvm::uvm_driver<my_item>(name), ap("ap") {}

    void run_phase(uvm::uvm_phase& phase) override {
        my_item req;

        while (true) {
            // 1. Get the next transaction from the sequencer
            seq_item_port.get_next_item(req);

            // 2. Drive the protocol (simulate delay)
            UVM_INFO("DRIVER", ("Driving data: " + std::to_string(req.data)).c_str(), uvm::UVM_LOW);
            sc_core::wait(10, sc_core::SC_NS);

            // 3. Broadcast to scoreboards via TLM analysis port
            ap.write(req);

            // 4. Signal completion back to the sequence
            seq_item_port.item_done();
        }
    }
};

// 4. A Mock Scoreboard (Subscribes to Analysis Port)
class my_scoreboard : public uvm::uvm_subscriber<my_item> {
public:
    UVM_COMPONENT_UTILS(my_scoreboard);

    my_scoreboard(uvm::uvm_component_name name) : uvm::uvm_subscriber<my_item>(name) {}

    // This is called automatically when the driver calls ap.write()
    void write(const my_item& t) override {
        UVM_INFO("SCOREBOARD", ("Received data: " + std::to_string(t.data)).c_str(), uvm::UVM_LOW);
    }
};

// 5. The Top-Level Test
class test_tlm : public uvm::uvm_test {
public:
    UVM_COMPONENT_UTILS(test_tlm);

    uvm::uvm_sequencer<my_item>* sqr;
    my_driver* drv;
    my_scoreboard* sb;

    test_tlm(uvm::uvm_component_name name) : uvm::uvm_test(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_test::build_phase(phase);
        sqr = uvm::uvm_sequencer<my_item>::type_id::create("sqr", this);
        drv = my_driver::type_id::create("drv", this);
        sb = my_scoreboard::type_id::create("sb", this);
    }

    void connect_phase(uvm::uvm_phase& phase) override {
        // Connect sequencer to driver
        drv->seq_item_port.connect(sqr->seq_item_export);

        // Connect driver's analysis port to scoreboard's analysis export
        drv->ap.connect(sb->analysis_export);
    }

    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);

        // Start the sequence on the sequencer
        my_sequence* seq = my_sequence::type_id::create("seq");
        seq->start(sqr);

        phase.drop_objection(this);
    }
};

int sc_main(int argc, char* argv[]) {
    uvm::run_test("test_tlm");
    return 0;
}
```

### Why Use `uvm_analysis_port`?

The `uvm_analysis_port` provides a **publish-subscribe** (broadcast) pattern. A monitor or driver calls `.write(transaction)` on the port without knowing who is listening.

You can connect zero, one, or ten scoreboards to that single analysis port during the `connect_phase`. If no one is connected, the `.write()` call safely returns immediately. This drastically improves the reusability of monitors and drivers, as they never have hardcoded dependencies on specific checking logic.

## Under the Hood: Analysis Ports and Sequencer Blocking

The abstraction of TLM relies heavily on standard C++ mechanisms within the SystemC core.

**`uvm_analysis_port` internals:**
When you `connect()` a subscriber to an analysis port, the port internally pushes a pointer to the subscriber's `uvm_analysis_imp` interface into an `std::vector` (or equivalent list). When the driver calls `ap.write(req)`, the analysis port iterates through this vector, executing a direct, zero-time C++ virtual function call to `write(req)` on every registered observer sequentially. If the vector is empty, the loop instantly terminates, making unconnected ports extremely lightweight.

**`get_next_item()` blocking mechanics:**
When `seq_item_port.get_next_item(req)` is called, it crosses the TLM interface boundary into the connected `uvm_sequencer`. Internally, the sequencer checks its request FIFO (which stores transactions generated by `finish_item()`). If the FIFO is empty, the sequencer calls `sc_core::wait(m_req_event)`. This suspends the driver's thread in the SystemC scheduler. Later, when the sequence finally produces an item, the sequencer pushes it to the FIFO and calls `m_req_event.notify()`, waking up the driver's thread to consume the transaction.

## Lesson 83: UVM-SystemC Bridge: Reporting, Registers, and Final Checks

Canonical lesson: https://www.learn-systemc.com/tutorials/080-uvm-systemc-bridge-reporting-registers-and-final-c

UVM reporting, verbosity, register abstraction, prediction, scoreboards, and end-of-test discipline.

## How to Read This Lesson

# UVM-SystemC Bridge: Reporting, Registers, and Final Checks

A sophisticated verification environment requires disciplined logging, standardized register access mechanisms, and rigorous end-of-test evaluations. Silent success is the enemy of verification; a test only passes if it can explicitly prove *why* it passed.

## Standard and source context

## UVM Reporting

UVM-SystemC replaces `std::cout` and `SC_REPORT_*` with a highly configurable, hierarchical reporting system via the `uvm_report_server`. Every message is qualified by:

- **Severity**: `UVM_INFO`, `UVM_WARNING`, `UVM_ERROR`, `UVM_FATAL`.
- **Verbosity**: `UVM_NONE`, `UVM_LOW`, `UVM_MEDIUM`, `UVM_HIGH`, `UVM_FULL`, `UVM_DEBUG`.
- **Context**: The hierarchical path of the component generating the message.
- **ID**: A string tag identifying the message category (e.g., "SCOREBOARD", "AXI_DRIVER").

You can filter messages globally or per-component using `set_report_verbosity_level()`, allowing you to keep normal test logs concise while enabling deep debug traces when a failure occurs.

## Register Abstraction Layer (RAL)

Modern SoCs contain thousands of control and status registers. Hardcoding their addresses in sequences makes tests brittle.

The UVM Register Abstraction Layer (RAL) provides an object-oriented mirror of the hardware's register map. Instead of generating a raw bus transaction to address `0x1004`, a sequence calls:
`my_reg_block.timer_ctrl.enable.write(status, 1);`

The RAL automatically translates this into the correct bus protocol transaction (via an adapter), issues the read/write, and updates its internal **predicted** state. This allows scoreboards to instantly check if the hardware's reset values and read/write policies (e.g., write-one-to-clear) match the specification.

## End-of-Test Discipline

A UVM simulation does not merely "stop" when the sequences finish. It transitions into final validation phases:

- **`extract_phase`**: Retrieves final coverage data and scoreboard queues.
- **`check_phase`**: Evaluates final assertions. Are there any pending transactions that were never completed? Did the scoreboard reconcile all expected data?
- **`report_phase`**: Summarizes the run.

Most importantly, you must query the `uvm_report_server` to verify that zero `UVM_ERROR` or `UVM_FATAL` messages were logged. A test with fatal exceptions is obviously broken, but a test that logs a `UVM_ERROR` and continues running must still be marked as a failure at the end.

## Complete Example: Reporting and Final Checks

The following complete `sc_main` example demonstrates how to configure verbosity, log various severities, and perform a strict end-of-test check using the `uvm_report_server` during the `report_phase`.

```cpp
#include <systemc>
#include <uvm>

class mock_scoreboard : public uvm::uvm_component {
public:
    UVM_COMPONENT_UTILS(mock_scoreboard);

    int expected_packets = 5;
    int received_packets = 3; // Intentionally causing a mismatch

    mock_scoreboard(uvm::uvm_component_name name) : uvm::uvm_component(name) {}

    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);

        // This will print because default verbosity is UVM_MEDIUM
        UVM_INFO("SCOREBOARD", "Starting packet processing...", uvm::UVM_MEDIUM);

        // This will NOT print unless verbosity is raised to UVM_HIGH
        UVM_INFO("SCOREBOARD", "Processing internal data block 1...", uvm::UVM_HIGH);

        sc_core::wait(10, sc_core::SC_NS);

        // We log an error, but the simulation CONTINUES running!
        UVM_ERROR("SCOREBOARD", "Data corruption detected in packet payload!");

        phase.drop_objection(this);
    }

    void check_phase(uvm::uvm_phase& phase) override {
        UVM_INFO("SCOREBOARD", "Executing Check Phase...", uvm::UVM_LOW);

        if (expected_packets != received_packets) {
            UVM_ERROR("SCOREBOARD", "Packet count mismatch at end of test!");
        }
    }
};

class test_reporting : public uvm::uvm_test {
public:
    UVM_COMPONENT_UTILS(test_reporting);

    mock_scoreboard* sb;

    test_reporting(uvm::uvm_component_name name) : uvm::uvm_test(name) {}

    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_test::build_phase(phase);

        sb = mock_scoreboard::type_id::create("sb", this);

        // Dynamically elevate verbosity for the scoreboard specifically
        sb->set_report_verbosity_level(uvm::UVM_HIGH);
    }

    // The Report Phase is the ultimate arbiter of test success
    void report_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_report_server* server = uvm::uvm_report_server::get_server();

        int err_count = server->get_severity_count(uvm::UVM_ERROR);
        int fatal_count = server->get_severity_count(uvm::UVM_FATAL);

        std::cout << "\n===================================================\n";
        std::cout << "               FINAL TEST SUMMARY                  \n";
        std::cout << "===================================================\n";

        if (err_count == 0 && fatal_count == 0) {
            std::cout << "TEST PASSED!\n";
            UVM_INFO("TEST", "Simulation completed with 0 errors.", uvm::UVM_NONE);
        } else {
            std::cout << "TEST FAILED! (Errors: " << err_count
                      << ", Fatals: " << fatal_count << ")\n";
            UVM_INFO("TEST", "Please review the log for failure details.", uvm::UVM_NONE);
        }
        std::cout << "===================================================\n";
    }
};

int sc_main(int argc, char* argv[]) {
    // Run the test.
    // It will generate errors, which will be caught in the report_phase.
    uvm::run_test("test_reporting");
    return 0;
}
```

### Why `UVM_ERROR` doesn't stop the simulation
By default, `UVM_FATAL` terminates the simulation immediately, while `UVM_ERROR` increments an error counter and allows the simulation to proceed. This is a deliberate design choice: it allows the testbench to uncover and report *multiple* independent failures in a single run, rather than halting at the very first bug it encounters. The test is ultimately failed during the `report_phase` by inspecting the server's error count.

## Under the Hood: Macro Optimization and RAL Blocking

The UVM-SystemC macros provide significant C++ performance optimizations. When you write `UVM_INFO("ID", "Complex string " + std::to_string(val), uvm::UVM_HIGH);`, the macro explicitly checks the component's verbosity *before* evaluating the string concatenation arguments. Under the hood, it roughly expands to: `if (uvm_report_enabled(uvm::UVM_HIGH, uvm::UVM_INFO, "ID")) uvm_report_info("ID", ...);`. This ensures that heavy string formatting operations in your testbench cost zero CPU cycles if the verbosity level is too low to print them.

Regarding the Register Abstraction Layer (RAL), when a sequence calls `my_reg.write(status, value)`, it appears as a simple synchronous C++ function call. However, underneath, it is a complex blocking operation:
1. The RAL generates a dynamic `uvm_reg_item` containing the read/write intent.
2. It passes this item to the connected `uvm_reg_adapter`, which dynamically instantiates and returns a concrete bus transaction (e.g., an AXI `uvm_sequence_item`).
3. The RAL forwards this transaction to the `uvm_sequencer` and immediately calls `sc_core::wait()` on an internal `sc_event`.
4. Once the driver finishes the TLM phase and calls `item_done()`, the sequencer fires the event, waking up the RAL thread.
5. The RAL updates its internal mirror of the register and finally returns the C++ function call to the sequence.

## Lesson 84: The UVM-SystemC Register Layer

Canonical lesson: https://www.learn-systemc.com/tutorials/081-the-uvm-systemc-register-layer

How to map memory-mapped registers into UVM-SystemC using uvm_reg, uvm_reg_map, and frontdoor adapters.

## How to Read This Lesson

# UVM-SystemC Register Layer (`uvm_reg`)

The Register Abstraction Layer (RAL) is arguably the most powerful feature of UVM. It allows you to create an abstract, object-oriented model of your hardware's memory-mapped registers, decoupling your test sequences from the physical bus protocol (e.g., TLM-2.0, APB, AXI).

In UVM-SystemC, the `uvm_reg` classes behave identically to their SystemVerilog counterparts. Let's dig into the Accellera UVM-SystemC source code to see how these abstractions are actually executed under the hood.

## Standard and source context

## 1. Defining a Register

Registers are defined by inheriting from `uvm_reg` and instantiating `uvm_reg_field` objects inside the `build()` method.

When you call `configure()` on a `uvm_reg_field`, the UVM-SystemC kernel allocates internal data structures to track both the **mirrored value** (what the testbench *thinks* the hardware holds) and the **desired value** (what the testbench *wants* to write to the hardware).

```cpp
#include <systemc>
#include <uvm>

class ctrl_reg : public uvm::uvm_reg {
public:
    uvm::uvm_reg_field* enable;
    uvm::uvm_reg_field* irq_mask;

    UVM_OBJECT_UTILS(ctrl_reg);

    ctrl_reg(const std::string& name = "ctrl_reg")
        : uvm::uvm_reg(name, 32, uvm::UVM_NO_COVERAGE) {}

    virtual void build() {
        enable = uvm::uvm_reg_field::type_id::create("enable");
        // Parameters: parent, size, lsb_pos, access, volatile, reset, has_reset, is_rand, obj
        enable->configure(this, 1, 0, "RW", 0, 0, 1, 1, nullptr);

        irq_mask = uvm::uvm_reg_field::type_id::create("irq_mask");
        irq_mask->configure(this, 8, 8, "RW", 0, 0xFF, 1, 1, nullptr);
    }
};
```

## 2. Assembling the Register Block

Registers are grouped into a `uvm_reg_block`, which contains a `uvm_reg_map` that defines their physical addresses.

When you call `map->add_reg()`, the kernel updates an internal memory map (`std::map<uint64_t, uvm_reg*>`). The `lock_model()` method seals the block, preventing further additions and caching the hierarchical paths for fast lookup.

```cpp
class sys_reg_block : public uvm::uvm_reg_block {
public:
    ctrl_reg* ctrl;
    uvm::uvm_reg_map* map;

    UVM_OBJECT_UTILS(sys_reg_block);

    sys_reg_block(const std::string& name = "sys_reg_block")
        : uvm::uvm_reg_block(name, uvm::UVM_NO_COVERAGE) {}

    virtual void build() {
        ctrl = ctrl_reg::type_id::create("ctrl");
        ctrl->configure(this, nullptr);
        ctrl->build();

        // Create the memory map: name, base_addr, bus_width (bytes), endianness
        map = create_map("map", 0x0000, 4, uvm::UVM_LITTLE_ENDIAN);

        // Add register to map at offset 0x10, with RW access
        map->add_reg(ctrl, 0x10, "RW");

        lock_model(); // Seal the RAL block
    }
};
```

## 3. The Adapter (Bridging RAL and TLM)

When a sequence calls `ctrl->write(status, 0x1)`, the RAL does not instantly write to the bus. Instead, the UVM-SystemC kernel performs the following steps:
1. `uvm_reg::write()` calls the internal `do_write()` method on the `uvm_reg_map`.
2. `do_write()` allocates a generic `uvm_reg_item` object containing the address, data, and command.
3. This `uvm_reg_item` is passed to the `reg2bus` method of a user-defined `uvm_reg_adapter`.
4. The adapter converts it into your specific bus transaction (e.g., a `tlm_generic_payload`).
5. The RAL pushes the converted transaction to the Sequencer, and blocks using `wait()` until the transaction is executed by the Driver.
6. Upon completion, the RAL calls `bus2reg` on the adapter to extract the response and updates the internal `m_mirrored` variable.

```cpp
// (Pseudocode for Adapter)
class tlm_reg_adapter : public uvm::uvm_reg_adapter {
    virtual uvm::uvm_sequence_item* reg2bus(const uvm::uvm_reg_bus_op& rw) {
        // Convert 'rw' to your bus transaction
        // e.g., allocate a tlm_generic_payload
    }
    virtual void bus2reg(uvm::uvm_sequence_item* bus_item, uvm::uvm_reg_bus_op& rw) {
        // Convert your bus transaction back to 'rw'
        // e.g., extract response status from tlm_generic_payload
    }
};
```

This elegant layering guarantees that if the hardware team changes the bus architecture from APB to TLM-2.0, you only need to rewrite the `tlm_reg_adapter`. The high-level tests calling `ctrl->write()` remain completely untouched!

```cpp
int sc_main(int argc, char* argv[]) {
    // Standard UVM execution
    uvm::uvm_root::get()->run_test();
    return 0;
}
```

## Source-reading checkpoint

For register-layer work, inspect `src/uvmsc` around register classes, adapters, and `uvm_report` calls. The implementation path helps connect mirrored state to real bus activity.

## Lesson 85: UVM-SystemC Sequence Item Macros

Canonical lesson: https://www.learn-systemc.com/tutorials/082-uvm-systemc-sequence-item-macros

How to register sequence items with the UVM factory and implement deep copy, compare, and print functions using UVM macros.

## How to Read This Lesson

# Sequence Items and the UVM Factory

In UVM-SystemC, data objects that flow through the sequencer to the driver are called `uvm_sequence_item`s. Because these items are highly dynamic (created, copied, and randomized constantly), they must be registered with the UVM Factory.

While you *could* manually write the `do_copy`, `do_print`, and `do_compare` methods, UVM-SystemC provides macros to automate this.

## Standard and source context

## The C++ Kernel Reality: Macro Expansion

To a C++ engineer, macros can be dangerous, but in UVM-SystemC they are strictly necessary to mimic SystemVerilog reflection.

If you expand the Accellera macro `UVM_OBJECT_UTILS_BEGIN(bus_transaction)`, it injects a static `uvm_object_registry<bus_transaction>` typedef inside your class. This registry inserts your class into the global UVM factory map before `sc_main` even begins. It also overrides virtual methods like `get_type_name()`, `create()`, and `clone()`.

The `UVM_FIELD_INT` macro goes further. It overrides the internal `__m_uvm_field_automation()` callback. When you call `copy()`, `print()`, or `pack()`, the kernel passes a `uvm_packer` or `uvm_printer` object to this callback. The macro injects code that dynamically pushes your C++ member variables into these packer objects based on the bitwise flags (like `UVM_ALL_ON | UVM_HEX`).

## The Field Macros

Here is a complete, compilable example showing how to build a TLM-style bus transaction using UVM macros.

```cpp
#include <systemc>
#include <uvm>

class bus_transaction : public uvm::uvm_sequence_item {
public:
    unsigned int address;
    unsigned int data;
    bool is_read;

    // 1. Register with the factory and open the automation block
    // Injects static uvm_object_registry and virtual overrides
    UVM_OBJECT_UTILS_BEGIN(bus_transaction);
        // 2. Automate copy, compare, and print
        // Injects code into __m_uvm_field_automation(uvm_object_wrapper&, int)
        UVM_FIELD_INT(address, uvm::UVM_ALL_ON | uvm::UVM_HEX);
        UVM_FIELD_INT(data, uvm::UVM_ALL_ON | uvm::UVM_HEX);
        UVM_FIELD_INT(is_read, uvm::UVM_ALL_ON);
    UVM_OBJECT_UTILS_END

    // Constructor required by UVM
    bus_transaction(const std::string& name = "bus_transaction")
        : uvm::uvm_sequence_item(name), address(0), data(0), is_read(false) {}
};

class test_env : public uvm::uvm_env {
public:
    UVM_COMPONENT_UTILS(test_env);

    test_env(uvm::uvm_component_name name) : uvm::uvm_env(name) {}

    void run_phase(uvm::uvm_phase& phase) {
        phase.raise_objection(this);

        // Factory creation (utilizes the registry injected by UVM_OBJECT_UTILS)
        bus_transaction* tx1 = bus_transaction::type_id::create("tx1");
        tx1->address = 0x1000;
        tx1->data = 0xDEADBEEF;

        bus_transaction* tx2 = bus_transaction::type_id::create("tx2");

        // Automated deep copy (utilizes __m_uvm_field_automation)
        tx2->copy(tx1);

        // Automated compare
        if (tx2->compare(tx1)) {
            UVM_INFO("TEST", "Transactions match exactly!", uvm::UVM_LOW);
        }

        // Automated print
        tx1->print();

        phase.drop_objection(this);
    }
};

class my_test : public uvm::uvm_test {
public:
    test_env* env;
    UVM_COMPONENT_UTILS(my_test);

    my_test(uvm::uvm_component_name name) : uvm::uvm_test(name) {}

    void build_phase(uvm::uvm_phase& phase) {
        env = test_env::type_id::create("env", this);
    }
};

int sc_main(int argc, char* argv[]) {
    uvm::uvm_root::get()->run_test("my_test");
    return 0;
}
```

## Output
When you execute `tx1->print()`, the `UVM_FIELD_*` macros automatically format the output into an organized table showing the hierarchy, type, size, and value of every field! This saves thousands of lines of boilerplate code in complex verification environments.

## Source-reading checkpoint

For sequence-item macros, inspect `src/uvmsc` around factory macros and `uvm_component_registry`. Expand the macro mentally until the registration behavior is ordinary C++ again.

## Lesson 86: UVM-SystemC Objections and Phase Drain Time

Canonical lesson: https://www.learn-systemc.com/tutorials/083-uvm-systemc-objections-and-phase-drain-time

How objections keep phases alive, how drain time prevents premature completion, and how this interacts with the SystemC scheduler.

## How to Read This Lesson

Phases are not magic time regions. They are methodology objects coordinated on top of the SystemC scheduler. Objections are the mechanism that tells a phase, "do not finish yet; useful verification work is still active."

## Standard and source context

Use `Docs/LRMs/uvm-systemc-language-reference-manual.pdf` for UVM-SystemC phasing and synchronization behavior. In source, inspect `Accellera SystemC GitHub repository/uvm-systemc/src/uvmsc`, especially phase, component, report, and objection-related implementation files.

## The Problem Objections Solve

In a testbench, many components may start activity during a run phase:

- sequences generate stimulus
- drivers consume items
- monitors observe interfaces
- scoreboards wait for expected responses

If the phase ended as soon as the top-level thread returned, the test could finish while responses are still in flight.

An objection says: this component has active work. Keep the phase alive until the objection is dropped.

## Basic Pattern

```cpp
void run_phase(uvm::uvm_phase& phase) override {
  phase.raise_objection(this);

  // start stimulus and wait for expected completion
  wait(100, sc_core::SC_NS);

  phase.drop_objection(this);
}
```

The exact API shape depends on the UVM-SystemC release and class context, but the modeling intent is stable: raise before activity, drop after activity.

## Drain Time

Drain time handles a subtle issue: a component may drop its final objection, but other SystemC activity triggered by that work may still need a small amount of simulation time to settle.

Drain time gives the phase a controlled grace period before it concludes. It should be used carefully. If you need a large drain time, the testbench probably lacks a proper completion condition.

## How This Interacts with SystemC

UVM-SystemC phasing is implemented in C++ on top of SystemC. The SystemC kernel still controls process execution, waits, events, and time advancement. UVM phases coordinate methodology-level state; they do not replace `sc_start()` semantics.

That means you debug phase hangs like this:

1. Which component still has an objection?
2. Which SystemC process is that component waiting in?
3. Which event or time delay should wake it?
4. Did a sequence, driver, or scoreboard miss a completion signal?

## Review Checklist

- Is every raised objection dropped on all control paths?
- Are exceptions or early returns handled?
- Is drain time small and justified?
- Does the test have a real completion condition?
- Can reports identify the component that holds the final objection?

## Lesson 87: UVM-SystemC API, Phase, Register, and Source Field Guide

Canonical lesson: https://www.learn-systemc.com/tutorials/135-uvm-systemc-api-phase-register-and-source-field-guide

Individual UVM-SystemC symbols for components, phases, reports, sequences, callbacks, resources, and register-layer source reading.

## How to Read This Lesson

UVM-SystemC layers a verification methodology on top of SystemC. Read names by role: object, component, phase, report, sequence, resource/configuration, TLM connection, or register model.

## Standard and source context

Use `Docs/LRMs/uvm-systemc-language-reference-manual.pdf`. Use `.codex-src/uvm-systemc/src/uvmsc` and `.codex-src/uvm-systemc/src/uvm.h` for implementation reading.

## Object and Component Families

`uvm_object`, `uvm_component`, `uvm_component_registry`, `uvm_object_registry`, `uvm_test`, `uvm_env`, `uvm_agent`, `uvm_monitor`, `uvm_driver`, and `uvm_sequencer` define the verification hierarchy.

`uvm_objects`, `uvm_object_globals`, `uvm_object_string_pool`, `uvm_typeid_base`, `uvm_queue`, and `uvm_scope_stack` are source-side helpers around object identity, storage, type lookup, and hierarchical printing.

Use `uvm_object` for data-like things that need factory, print, copy, compare, pack, or record behavior. Use `uvm_component` for hierarchical verification structure with phases and parent/child ownership.

## Factory and Core Services

`uvm_factory`, `uvm_coreservices_t`, `uvm_default_coreservice_t`, `uvm_default_coreservices_t`, and factory registration macros are the override machinery. The factory is what lets a test replace a component or sequence type without rewriting the environment.

That flexibility is useful only when type names, instance paths, and override policy are documented. Otherwise factory use becomes a late-bound mystery.

## Phases, Domains, and Objections

`uvm_phase`, `uvm_process_phase`, `uvm_domain`, `build_phase`, `connect_phase`, `run_phase`, phase callbacks, `uvm_objection`, and drain time control test execution.

`uvm_phase_state`, `uvm_phase_states`, `uvm_phase_type`, `uvm_phase_get_run_count`, `uvm_run_phase`, `uvm_pre_reset_phase`, `uvm_reset_phase`, `uvm_post_reset_phase`, `uvm_pre_configure_phase`, `uvm_configure_phase`, `uvm_main_phase`, and `uvm_post_shutdown_phase` give names to the phase graph and lifecycle states. `uvm_objection_event` is part of the objection notification story.

The remaining runtime phase names include `uvm_post_configure_phase`, `uvm_pre_main_phase`, `uvm_post_main_phase`, `uvm_pre_shutdown_phase`, `uvm_shutdown_phase`, `uvm_end_of_elaboration_phase`, `uvm_start_of_simulation_phase`, `uvm_extract_phase`, `uvm_check_phase`, `uvm_report_phase`, and `uvm_final_phase`. They make the UVM lifecycle explicit instead of hiding it inside one monolithic run function.

Build and connect phases prepare the structure. Run-time phases execute behavior. Objections tell the scheduler that useful work is still active. If a simulation never ends, inspect objections first. If it ends too early, inspect who failed to raise one.

## Reporting and Message APIs

`uvm_report`, `uvm_report_warning`, `uvm_report_error`, `uvm_report_fatal`, `uvm_severity`, `uvm_action`, `uvm_default_report_server`, `uvm_report_catcher_data`, and report catchers define how verification code communicates failures.

`uvm_report_message_element_base`, `uvm_report_message_element_container`, `uvm_message_defines`, and `uvm_globals` are implementation and macro-support names that show how messages are assembled and shared across the library.

`uvm_report_message_int_element`, `uvm_report_message_string_element`, `uvm_severity_name`, `uvm_severity_type`, `uvm_verbosity`, `uvm_tr_database`, and `uvm_status_container` are reporting, transaction recording, and status-support names. They matter when a verification environment needs machine-readable debug output.

Prefer structured reports over raw output. A good report carries severity, component path, message ID, and enough context to reproduce the failure.

## Sequence and TLM APIs

`uvm_sequence`, `uvm_sequence_base`, `uvm_sequence_item`, `uvm_sequence_item` macros, `uvm_sequencer_base`, `uvm_driver`, `uvm_blocking_get_port`, `uvm_blocking_get_peek_port`, `uvm_blocking_peek_port`, `uvm_export_base`, and UVM TLM ports/exports move stimulus and observations through the environment.

`uvm_nonblocking_get_port`, `uvm_nonblocking_put_port`, `uvm_nonblocking_peek_port`, `uvm_nonblocking_get_peek_port`, and `uvm_sequencer_param_base` are the non-blocking and parameterized sides of the same communication family.

`uvm_seq_item_pull_imp`, `uvm_sequence_request`, `uvm_sequence_state_enum`, `uvm_sqr_if_base`, `uvm_tlm_gp`, `uvm_wait_op`, and `uvm_sc_if` are sequence/TLM adapter names. If a driver and sequencer disagree, these are the kinds of interfaces you eventually trace.

The sequence/sequencer/driver split exists to separate intent from pin-level or transaction-level driving. Review that boundary carefully. A sequence should describe what to try; a driver should know how to drive it.

## Configuration, Resources, and Callbacks

`uvm_config_db`, `uvm_config_int`, `uvm_config_string`, `uvm_config_object`, `uvm_config_wrapper`, `uvm_resource_types`, `uvm_callback`, `uvm_callback_iter`, `uvm_callbacks`, and `uvm_callbacks_base` provide late-bound configuration and extension points.

`uvm_resource_db`, `uvm_typed_callbacks`, `uvm_event`, `uvm_event_callback`, `uvm_get_to_lock_dap`, `uvm_simple_lock_dap`, and `uvm_factory_override` are source-level names around resources, callbacks, synchronization events, data-access policy, and factory override records.

`uvm_resource_db_options`, `uvm_resource_options`, `uvm_set_before_get_dap`, `uvm_set_get_dap_base`, `uvm_set_config_int`, and `uvm_set_config_string` are the option and legacy configuration names you should recognize when maintaining older UVM-style environments.

Configuration database use should be visible in the environment contract. Callback use should be treated like plugin architecture: powerful, but easy to make invisible.

## Register Layer Names

`uvm_reg`, `uvm_reg_data_t`, `uvm_reg_addr_t`, `uvm_status_e`, `uvm_reg_map_info`, `uvm_reg_file`, `uvm_reg_backdoor`, `uvm_reg_cbs`, `uvm_reg_cvr_t`, `uvm_hdl_path_concat`, `uvm_mem`, `uvm_mem_region`, `uvm_mem_mam`, `uvm_vreg`, `uvm_vreg_field`, and `uvm_path_e` belong to register and memory modeling.

`uvm_access_e`, `uvm_predict_e`, `uvm_reg_access_seq`, `uvm_reg_bit_bash_seq`, `uvm_reg_sequence`, `uvm_reg_frontdoor`, `uvm_reg_indirect_data`, `uvm_reg_read_only_cbs`, `uvm_reg_write_only_cbs`, `uvm_reg_addr_logic_t`, `uvm_reg_data_logic_t`, `uvm_reg_byte_en_t`, `uvm_mem_access_seq`, `uvm_mem_walk_seq`, `uvm_mem_mam_cfg`, `uvm_mem_mam_policy`, `uvm_vreg_field_cbs`, `uvm_hdl_path_slice`, and `uvm_mask_size` are the deeper register/memory test and access-policy vocabulary.

`uvm_reg_model`, `uvm_reg_fields`, `uvm_reg_fifo`, `uvm_reg_hw_reset_seq`, `uvm_reg_mem_shared_access_seq`, `uvm_reg_mem_tests_e`, `uvm_reg_predictor`, `uvm_reg_sequence_inst`, `uvm_reg_tlm_adapter`, `uvm_reg_field_cb_iter`, `uvm_vreg_cbs`, and `uvm_reg_frontdoor` are register-layer source and sequence names. They are how the library connects register abstractions to bus operations, prediction, reset tests, memories, and callbacks.

The register layer exists to keep tests from hardcoding every bus detail. The model should know address maps, access policy, reset values, mirroring, frontdoor paths, and backdoor paths.

## Printer, Packer, Recorder, and Compare Policy

`uvm_default_printer`, `uvm_default_table_printer`, `uvm_default_tree_printer`, `uvm_default_line_printer`, `uvm_default_packer`, `uvm_default_recorder`, `uvm_default_comparer`, `uvm_bitstream_t`, `uvm_check_e`, `uvm_coverage_model_e`, `uvm_endianness_e`, and `uvm_elem_kind_e` support generic object operations.

Also recognize `uvm_recorder`, `uvm_line_printer`, `uvm_printer_knobs`, `uvm_printer_row_info`, `uvm_field_enum`, `uvm_radix_enum`, `uvm_integral_t`, `uvm_recursion_policy_enum`, `uvm_active_passive_enum`, `uvm_hier_e`, `uvm_apprepend`, and the misspelled `uvm_componet_name` token that can appear in older/generated material.

`uvm_table_printer`, `uvm_tree_printer`, `uvm_tre_printer`, `uvm_void`, `uvm_object_defines`, `uvm_derived_callbacks`, `uvm_phase_queue`, and `uvm_factory_queue_class` are printer, macro, callback, phase-queue, and factory-support names. Treat them as implementation and utility vocabulary unless your verification environment customizes those services directly.

These names matter when a test fails and you need readable data. If sequence items do not print, compare, pack, or record clearly, debug time grows quickly.

## Senior Review Checklist

- Can you identify every component's parent and phase responsibilities?
- Are objections balanced?
- Is factory override policy intentional?
- Is config-db use documented by path and type?
- Does the register model reflect firmware-visible truth?

## Lesson 88: The Execution Phases

Canonical lesson: https://www.learn-systemc.com/tutorials/084-the-execution-phases

A deep dive into the SystemC execution model: Elaboration, Initialization, Evaluation, Update, and advancing time.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# The Execution Phases

The SystemC Language Reference Manual (IEEE 1666) defines a strict execution model that governs how your C++ code behaves like parallel hardware. Understanding these phases is the key to mastering SystemC.

### Under the Hood: C++ Implementation in Accellera SystemC

To understand how a sequential C++ program simulates parallel hardware, look at the core of the Accellera `systemc` repository (`src/sysc/kernel/sc_simcontext.cpp`):

1. **`sc_simcontext`:** This is the heart of the SystemC kernel. It is a global singleton object that manages the simulation time, the hierarchy tree (`sc_object_manager`), and the event queues.
2. **The Runnable Queue (`sc_runnable`):** When an event triggers, the `sc_simcontext` pushes the sensitive `sc_process_b` (the base class for threads and methods) onto the `sc_runnable` queue. The evaluation phase is literally a `while (!runnable->is_empty())` loop that pops processes and executes them.
3. **Context Switching:** How does `sc_core::wait()` work? Accellera SystemC uses user-level coroutines (historically QuickThreads, now often ucontext or POSIX threads depending on the OS). When a thread calls `wait()`, the C++ execution context (registers, stack pointer) is saved, and control yields back to the `sc_simcontext` evaluation loop.
4. **The Update Queue:** Primitive channels (like `sc_signal`) inherit from `sc_prim_channel`. When you write a new value, the channel calls `request_update()`. This simply pushes a pointer to the channel onto the `m_update_list` array inside `sc_simcontext`. During the update phase, the kernel loops over this array and calls the virtual `update()` method on each channel.

## Standard and source context

## 1. Elaboration Phase
Before the simulation clock even starts ticking, SystemC builds the hierarchy.
- **Module Instantiation:** The constructors (`SC_CTOR` or custom constructors) of all `sc_module` derived classes are executed.
- **Port Binding:** Ports (`sc_in`, `sc_out`, `sc_port`) are bound to channels or interfaces.
- **Process Registration:** `SC_METHOD`, `SC_THREAD`, and `SC_CTHREAD` are registered with the kernel.

> [!WARNING]
> You cannot bind ports after the elaboration phase has completed. Once `sc_start()` is called, the hierarchy is locked.

## 2. Initialization Phase
When `sc_start()` is invoked, the kernel enters initialization:
- Every registered `SC_METHOD` and `SC_THREAD` is executed exactly once, unless `dont_initialize()` was explicitly called on it during elaboration.
- The processes run until they yield (e.g., hit a `wait()`) or return.

## 3. The Evaluation Phase
This is the core of the simulation loop.
- The scheduler selects a runnable process from the **runnable queue**.
- The process executes. If it writes to a signal (`sc_signal::write`), the new value is *not* immediately visible. Instead, the signal is added to the **update queue**.
- The evaluation phase continues until the runnable queue is entirely empty.

## 4. The Update Phase
Once all processes have suspended, the kernel processes the update queue.
- All primitive channels (like `sc_signal`) apply their pending writes. The "next" value becomes the "current" value.
- If the value changed, any process sensitive to that channel's events is pushed back onto the runnable queue.

## 5. Delta Cycles
If the update phase caused new processes to become runnable, the kernel returns to the Evaluation Phase *without advancing the simulation time*. This loop (Evaluate -> Update -> Evaluate) is called a **Delta Cycle**. It allows zero-delay combinational logic to settle deterministically.

## 6. Advancing Time
If the runnable queue and update queue are both empty, the kernel looks at the **event queue** for pending timed events (e.g., `wait(10, SC_NS)`).
- The simulation time is advanced to the timestamp of the earliest pending event.
- The processes waiting on that event are moved to the runnable queue.
- The cycle repeats.

## Complete Execution Example

Below is a complete, compilable `sc_main` example that demonstrates the elaboration, initialization, evaluation, and update phases in action.

```cpp
#include <systemc>
#include <iostream>

SC_MODULE(phase_demo) {
    sc_core::sc_signal<int> sig;

    SC_CTOR(phase_demo) : sig("sig") {
        std::cout << "1. Elaboration Phase: Constructor running." << std::endl;

        SC_THREAD(stimulus_thread);
        // We do NOT use dont_initialize() here, so it runs during initialization.

        SC_METHOD(monitor_method);
        sensitive << sig;
        dont_initialize(); // Only run when 'sig' actually changes
    }

    void stimulus_thread() {
        std::cout << "2. Initialization Phase: Thread running for the first time." << std::endl;

        // Wait for some time to pass
        sc_core::wait(10, sc_core::SC_NS);

        std::cout << "3. Evaluation Phase (@" << sc_core::sc_time_stamp()
                  << "): Thread writing to signal." << std::endl;

        // This schedules an update, but does not change the value immediately
        sig.write(42);

        std::cout << "   Immediate read after write (old value): " << sig.read() << std::endl;

        // Yield to allow the update phase and subsequent delta cycle to run
        sc_core::wait(sc_core::SC_ZERO_TIME);

        std::cout << "6. Evaluation Phase (Next Delta): Value is now updated to: "
                  << sig.read() << std::endl;
    }

    void monitor_method() {
        std::cout << "5. Evaluation Phase (Delta Cycle): Monitor triggered! Signal value is: "
                  << sig.read() << std::endl;
    }
};

int sc_main(int argc, char* argv[]) {
    std::cout << "Starting Elaboration..." << std::endl;
    phase_demo demo("demo");

    std::cout << "Starting Simulation..." << std::endl;
    sc_core::sc_start();

    return 0;
}
```

Running this program clearly illustrates how the SystemC kernel defers signal updates until the evaluation phase completes, ensuring deterministic hardware-like behavior in a sequential software environment.

## Lesson 89: Understanding Delta Cycles

Canonical lesson: https://www.learn-systemc.com/tutorials/085-understanding-delta-cycles

Why Delta Cycles exist, how they solve non-determinism, and how to debug zero-delay loops.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# Understanding Delta Cycles

A **Delta Cycle** is the cornerstone of SystemC's ability to model parallel hardware using a sequential software language (C++).

### Under the Hood: C++ Implementation in Accellera SystemC

To understand delta cycles, we must look at how the `sc_simcontext` scheduler evaluates time. In the Accellera SystemC repository, the scheduler has two primary time concepts: physical time (`sc_time`) and delta count (an integer).

When a process calls `sc_core::wait(sc_core::SC_ZERO_TIME)`, the kernel does *not* put that process into the standard timed event queue. Instead, it places the process into a special "delta event queue". After the current update phase completes, the kernel checks this delta queue. If it is not empty, it increments its internal `m_delta_count` variable, moves the processes from the delta queue to the runnable queue, and loops again *without* modifying the physical `m_curr_time` variable.

## Standard and source context

## The Non-Determinism Problem
Imagine two logic gates connected in series, executing in C++. If they are modeled as simple sequential assignments, the order of execution matters.

If Process A runs first and writes to a signal, and Process B runs second and reads it, the result is deterministic. But if the OS scheduler happens to run Process B first, Process B reads the *old* value of the signal. Real hardware evaluates simultaneously. Software evaluates sequentially, creating a race condition.

## The Delta Cycle Solution
SystemC solves this via the **Evaluate-Update** paradigm as mandated by the IEEE 1666 standard.

1. **Evaluate:** Both Process A and Process B run in an arbitrary order. Process A calls `Signal1.write(1)`. SystemC does *not* change the value of `Signal1`. Instead, it schedules the change in the update queue.
2. **Update:** Once all runnable processes finish and suspend, the kernel applies the writes in the update queue. `Signal1` officially becomes `1`.
3. **Delta:** Because `Signal1` changed, Process B (which is sensitive to it) wakes up. Time has *not* advanced. We are at `T=0 + 1 delta cycle`. Process B evaluates again, this time seeing the deterministic, updated value.

## Complete Delta Cycle Example

Below is a complete, fully compilable `sc_main` program that demonstrates how a race condition is avoided using delta cycles and `sc_signal`. It also demonstrates the use of `SC_ZERO_TIME` to explicitly yield a thread until the next delta cycle.

```cpp
#include <systemc>
#include <iostream>

SC_MODULE(delta_demo) {
    sc_core::sc_signal<bool> sig_a;
    sc_core::sc_signal<bool> sig_b;

    SC_CTOR(delta_demo) : sig_a("sig_a"), sig_b("sig_b") {
        SC_THREAD(driver_thread);

        SC_METHOD(combinational_method);
        sensitive << sig_a;
        dont_initialize();
    }

    void driver_thread() {
        // Time = 0 s, Delta = 0
        std::cout << "Delta 0: Driver writing true to sig_a" << std::endl;
        sig_a.write(true);

        // At this point, sig_a has NOT updated yet.
        // We yield for one delta cycle to let the Update Phase happen.
        sc_core::wait(sc_core::SC_ZERO_TIME);

        // Time = 0 s, Delta = 1
        std::cout << "Delta 1: Driver woke up from zero-time wait." << std::endl;

        // Wait for actual simulation time to advance
        sc_core::wait(10, sc_core::SC_NS);

        std::cout << "@" << sc_core::sc_time_stamp() << ": Simulation complete." << std::endl;
    }

    void combinational_method() {
        // This is triggered whenever sig_a actually updates.
        // It happens during Delta 1.
        std::cout << "Delta 1: Combinational method triggered! sig_a = "
                  << sig_a.read() << std::endl;

        // Write to another signal, triggering a second update phase (Delta 2)
        sig_b.write(!sig_a.read());
    }
};

int sc_main(int argc, char* argv[]) {
    delta_demo demo("demo");
    sc_core::sc_start();
    return 0;
}
```

## The Delta Delay `wait(SC_ZERO_TIME)`
As shown in the example, you sometimes need to explicitly force a thread process to yield until the next delta cycle without advancing physical time.
You can do this by waiting on `sc_core::SC_ZERO_TIME`.

> [!WARNING]
> **Infinite Delta Loops:** If Process A writes to a signal that wakes Process B, and Process B writes to a signal that wakes Process A, they will loop forever in zero time. The simulation will freeze without advancing the clock, resulting in a maximum delta cycle limit exception from the kernel. Always ensure combinational loops are broken by a clock edge, delay, or conditional logic that stops the oscillation!

## Source-reading checkpoint

If a delta-cycle explanation feels hand-wavy, inspect Accellera `sc_simcontext` and `request_update`. The source makes the evaluate/update boundary concrete.

## Lesson 90: Custom Hierarchical Channels

Canonical lesson: https://www.learn-systemc.com/tutorials/086-custom-hierarchical-channels

How to build complex protocols by inheriting from sc_channel and implementing sc_interface.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# Custom Hierarchical Channels

In SystemC, communication is separated from computation. Computation happens in `sc_module` processes, while communication is handled by channels implementing `sc_interface`.

While `sc_signal` and `sc_fifo` are primitive channels (they hook directly into the kernel's update phase without possessing their own processes), you can also build **Hierarchical Channels**.

### Under the Hood: C++ Implementation in Accellera SystemC

How does a hierarchical channel combine the behavior of a module and an interface? By leveraging C++ multiple inheritance.

1. **`sc_channel`:** If you look at the Accellera source code, `class sc_channel` is literally just a `typedef sc_module sc_channel;`. By inheriting from `sc_channel`, your class inherits `sc_object`, participates in the `sc_simcontext` hierarchy, and can register `SC_THREAD`s.
2. **`sc_interface`:** This is a pure abstract base class with zero state. It requires the implementation of a `register_port` callback.
3. **The Bridge:** When you write `class ahb_bus : public sc_channel, public bus_if`, you are bridging the static module hierarchy with dynamic TLM-like function calls. When an initiator module binds its `sc_port<bus_if>` to your `ahb_bus` instance, the SystemC kernel's `end_of_elaboration` phase checks that `ahb_bus` dynamically casts to `bus_if` and stores a pointer. When the initiator calls `port->burst_write()`, it dereferences that pointer and executes the C++ method *within the context* of the `ahb_bus` object's memory space.

## Standard and source context

## What is a Hierarchical Channel?
A hierarchical channel is essentially just an `sc_module` (technically derived from `sc_channel`, which is a typedef of `sc_module`) that also implements one or more `sc_interface` classes. Because it is a module, it can contain its own ports, signals, and processes!

This allows you to encapsulate complex bus protocols (like AXI, PCIe, or I2C), arbiters, or shared memories into a single reusable block.

## Complete Custom Bus Example

Below is a complete, fully compilable `sc_main` example demonstrating how to define an interface, implement it in a hierarchical channel equipped with a mutex for arbitration, and use it from an initiator module.

```cpp
#include <systemc>
#include <iostream>
#include <vector>

// 1. Define the Interface
class bus_if : virtual public sc_core::sc_interface {
public:
    virtual void burst_write(int addr, const std::vector<int>& data) = 0;
    virtual void burst_read(int addr, std::vector<int>& data, int len) = 0;
};

// 2. Implement the Hierarchical Channel
class ahb_bus_channel : public sc_core::sc_channel, public bus_if {
public:
    SC_HAS_PROCESS(ahb_bus_channel);

    ahb_bus_channel(sc_core::sc_module_name name) : sc_core::sc_channel(name) {
        // A hierarchical channel can have its own threads to manage background tasks
        SC_THREAD(monitor_thread);
    }

    // Implement the interface write method
    void burst_write(int addr, const std::vector<int>& data) override {
        // Lock the bus to prevent other initiators from interfering
        bus_mutex.lock();

        std::cout << "@" << sc_core::sc_time_stamp() << " BUS: Burst Write starting at address "
                  << addr << " for length " << data.size() << std::endl;

        // Simulate bus latency based on burst length
        sc_core::wait(10 * data.size(), sc_core::SC_NS);

        // Store data in fake memory
        for(size_t i = 0; i < data.size(); i++) {
            memory[addr + i] = data[i];
        }

        bus_mutex.unlock();
    }

    // Implement the interface read method
    void burst_read(int addr, std::vector<int>& data, int len) override {
        bus_mutex.lock();
        std::cout << "@" << sc_core::sc_time_stamp() << " BUS: Burst Read starting at address "
                  << addr << " for length " << len << std::endl;

        sc_core::wait(10 * len, sc_core::SC_NS);

        data.clear();
        for(int i = 0; i < len; i++) {
            data.push_back(memory[addr + i]);
        }

        bus_mutex.unlock();
    }

private:
    sc_core::sc_mutex bus_mutex;
    std::map<int, int> memory;

    void monitor_thread() {
        while(true) {
            sc_core::wait(100, sc_core::SC_NS);
            // Background monitoring logic could go here
        }
    }
};

// 3. Define an Initiator that uses the bus
SC_MODULE(initiator) {
    // Port bound to the custom bus interface
    sc_core::sc_port<bus_if> bus_port;

    SC_CTOR(initiator) {
        SC_THREAD(run);
    }

    void run() {
        sc_core::wait(5, sc_core::SC_NS);

        std::vector<int> write_data = {42, 43, 44};
        bus_port->burst_write(0x1000, write_data);

        std::vector<int> read_data;
        bus_port->burst_read(0x1000, read_data, 3);

        std::cout << "Initiator read back: ";
        for (int v : read_data) std::cout << v << " ";
        std::cout << std::endl;
    }
};

// 4. Top-level integration
int sc_main(int argc, char* argv[]) {
    // Instantiate the channel and the initiator
    ahb_bus_channel custom_bus("custom_bus");
    initiator init1("init1");

    // Bind the initiator's port directly to the hierarchical channel
    init1.bus_port(custom_bus);

    sc_core::sc_start(200, sc_core::SC_NS);
    return 0;
}
```

By using hierarchical channels, your IP blocks only need an `sc_port<bus_if>`. They call `port->burst_write()` without knowing or caring about the complex arbitration logic, mutexes, and delays happening inside the `ahb_bus_channel`. This encapsulation heavily promotes reuse and abstraction.

## Source-reading checkpoint

For a custom channel, read the Accellera `sc_prim_channel` path and its `request_update` hook. That is the implementation seam behind deferred channel updates.

## Lesson 91: Dynamic Processes (sc_spawn)

Canonical lesson: https://www.learn-systemc.com/tutorials/087-dynamic-processes-sc-spawn

How to dynamically create processes at runtime instead of relying solely on static elaboration.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# Dynamic Processes (`sc_spawn`)

Traditionally, SystemC processes (`SC_METHOD` and `SC_THREAD`) are static. They are registered during the elaboration phase (in the module's constructor) and exist for the entire duration of the simulation.

But what if you are building an RTOS model and need to dynamically spawn a thread when an interrupt fires? Or what if you want to spawn a watcher thread for every active TLM transaction?

Enter `sc_spawn`, introduced in IEEE 1666 to provide dynamic process capabilities.

### Under the Hood: C++ Implementation in Accellera SystemC

How does SystemC spawn a thread mid-simulation? The implementation relies heavily on C++ functors and the `sc_process_b` base class.

1. **`sc_bind` and Functors:** When you use `sc_bind`, SystemC leverages C++ template metaprogramming (similar to `std::bind`) to package your function pointer and its arguments into a single `sc_process_host` functor object.
2. **Dynamic Process Creation:** The `sc_spawn` function allocates a new `sc_thread_process` (or `sc_method_process`) object on the heap, passes it the functor, and registers it with the global `sc_simcontext`.
3. **`sc_process_handle` Reference Counting:** SystemC returns an `sc_process_handle` rather than a raw pointer. Why? Because dynamic processes can die. If the thread function returns or is killed, the `sc_thread_process` object is destroyed. The `sc_process_handle` acts like a smart pointer; it safely tracks whether the underlying process object is still valid (`handle.valid()`) to prevent segfaults when trying to kill an already-dead thread.

## Standard and source context

## Spawning a Thread
The `sc_core::sc_spawn` function allows you to create a new process dynamically at runtime. It accepts a callable (usually generated via `sc_bind`), a string name, and optional spawn configuration options.

## Process Handles (`sc_process_handle`)
When you spawn a process, `sc_spawn` returns an `sc_process_handle`. You can use this handle to control the dynamic process:
- `handle.suspend()`: Pauses the thread.
- `handle.resume()`: Resumes the thread.
- `handle.disable()`: Disables sensitivity.
- `handle.kill()`: Immediately terminates the thread and throws an `sc_unwind_exception`.

## Complete Dynamic Process Example

Below is a complete, fully compilable `sc_main` program demonstrating how to dynamically spawn threads, pass arguments to them via `sc_bind`, configure them as threads or methods, and manage their lifecycle using `sc_process_handle`.

```cpp
#include <systemc>
#include <iostream>
#include <vector>

// 1. The function we want to spawn dynamically
void my_dynamic_task(int id) {
    try {
        std::cout << "@" << sc_core::sc_time_stamp() << " Task " << id << " started!" << std::endl;

        // Consume some simulation time
        sc_core::wait(10, sc_core::SC_NS);

        std::cout << "@" << sc_core::sc_time_stamp() << " Task " << id << " finished!" << std::endl;
    } catch (const sc_core::sc_unwind_exception& e) {
        std::cout << "@" << sc_core::sc_time_stamp() << " Task " << id << " was killed!" << std::endl;
        // Optionally re-throw or handle cleanup
    }
}

// 2. The module that spawns other processes
class Spawner : public sc_core::sc_module {
public:
    SC_HAS_PROCESS(Spawner);

    std::vector<sc_core::sc_process_handle> handles;

    Spawner(sc_core::sc_module_name name) : sc_core::sc_module(name) {
        SC_THREAD(run_spawner);
    }

    void run_spawner() {
        sc_core::wait(5, sc_core::SC_NS);

        std::cout << "Spawner: Creating dynamic threads..." << std::endl;

        for (int i = 1; i <= 3; ++i) {
            // Configure the dynamic process
            sc_core::sc_spawn_options opt;
            // E.g., make it a method instead of a thread: opt.spawn_method();
            // opt.dont_initialize();

            std::string task_name = "dynamic_task_" + std::to_string(i);

            // Spawn a new thread dynamically and store the handle!
            sc_core::sc_process_handle h = sc_core::sc_spawn(
                sc_core::sc_bind(&my_dynamic_task, i),
                task_name.c_str(),
                &opt
            );
            handles.push_back(h);
        }

        // Wait a short time, then kill the second thread prematurely
        sc_core::wait(5, sc_core::SC_NS);
        std::cout << "Spawner: Killing Task 2 prematurely..." << std::endl;
        if (handles.size() >= 2 && handles[1].valid()) {
            handles[1].kill();
        }
    }
};

int sc_main(int argc, char* argv[]) {
    Spawner spawner_mod("spawner_mod");
    sc_core::sc_start(30, sc_core::SC_NS);
    return 0;
}
```

## Spawn Options
You can configure the dynamically spawned process using `sc_core::sc_spawn_options` object passed as the third parameter:
- `spawn_method()`: Force the spawned process to be an `SC_METHOD` instead of a thread. (Note: Methods cannot call `wait()`).
- `set_sensitivity()`: Make the dynamic process sensitive to specific events, just like static processes.
- `dont_initialize()`: Prevent the process from running immediately upon creation, meaning it waits for an event in its sensitivity list to trigger it first.

This makes `sc_spawn` incredibly powerful for modeling complex software stacks, dynamic thread pools in Virtual Platforms, and reactive sequence items in UVM!

## Lesson 92: Advanced Error Reporting

Canonical lesson: https://www.learn-systemc.com/tutorials/088-advanced-error-reporting

Mastering sc_report_handler, message severity, and overriding default kernel behaviors.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# Advanced Error Reporting

When building complex virtual platforms, simple `std::cout` statements are not enough. You need a structured way to report warnings, errors, and fatal faults, while allowing the top-level testbench to filter, log, or abort the simulation based on these reports. SystemC provides a robust reporting mechanism through `sc_core::sc_report_handler` and the associated `SC_REPORT_*` macros, specified in the IEEE 1666 Standard.

### Under the Hood: C++ Implementation in Accellera SystemC

To understand why `sc_report_handler` is so flexible, look at its C++ implementation:
1. **The Global Handler Map:** The `sc_report_handler` maintains internal static maps (like `std::map<std::string, sc_actions>`). When you call `sc_report_handler::set_actions("MyError", SC_DO_NOTHING)`, it stores this bitmask against the string key `"MyError"`.
2. **Macro Expansion:** The macros like `SC_REPORT_ERROR(msg_type, msg)` expand into a call to `sc_report_handler::report()`.
3. **Action Resolution:** Inside `report()`, the kernel checks its internal maps in a strict priority order:
   - Is there an action set for this specific `msg_type` string?
   - If not, is there an action set for this specific severity level (`SC_ERROR`)?
   - If not, what is the default action for this severity?
4. **Execution:** Once the `sc_action` bitmask is resolved, the handler checks the bits. If the `SC_THROW` bit is set, it dynamically allocates an `sc_report` object, populates it with the file name and line number (captured by the macro), and throws it as a C++ exception.

## Standard and source context

## LRM Standard Severity Levels

The SystemC Language Reference Manual (LRM) defines four strict severity levels, represented by the `sc_core::sc_severity` enumeration:

1. `SC_INFO`: Informative messages. By default, these are printed to the standard output and do not interrupt simulation.
2. `SC_WARNING`: Indicates a potentially incorrect condition. The simulation can continue, but the user should be aware. By default, it prints to standard output.
3. `SC_ERROR`: A definitive error condition. By default, this action is configured to `SC_DEFAULT_ERROR_ACTIONS`, which throws a C++ exception of type `sc_core::sc_report`.
4. `SC_FATAL`: A critical failure that cannot be recovered from. By default, this executes `SC_DEFAULT_FATAL_ACTIONS`, which prints the message and calls `abort()` to terminate the process immediately.

These levels are typically triggered using their corresponding macros:
- `SC_REPORT_INFO(msg_type, msg)`
- `SC_REPORT_WARNING(msg_type, msg)`
- `SC_REPORT_ERROR(msg_type, msg)`
- `SC_REPORT_FATAL(msg_type, msg)`

## The Message Type String (`msg_type`)

Notice the first argument in all macros: `msg_type`. This is a crucial LRM concept. It acts as a unique categorical identifier for the error.

By categorizing errors with a specific `msg_type`, you allow the end user or top-level integrator to override how those specific errors are handled across the entire simulation! A good practice is to use a hierarchical naming convention, such as `"/Company/IP/Component/ErrorCode"`.

## Overriding Report Actions (`sc_action`)

The true power of `sc_report_handler` is action overriding. Suppose an IP block you bought throws an `SC_REPORT_ERROR` because a packet was dropped. But in your specific testbench, dropping packets is expected (e.g., negative testing). You don't want the simulation to crash!

You can change the action for that specific `msg_type` or for an entire severity level.

### Available Actions (`sc_core::sc_action`)
Actions can be bitwise OR'd together (e.g., `SC_LOG | SC_DISPLAY`):
- `SC_DO_NOTHING`: Suppress the report entirely.
- `SC_THROW`: Throw an `sc_core::sc_report` exception.
- `SC_LOG`: Write the report to the configured log file.
- `SC_DISPLAY`: Print the report to the standard output.
- `SC_CACHE`: Save the report so it can be retrieved later via `sc_report_handler::get_cached_report()`.
- `SC_INTERRUPT`: Interrupt execution (useful for debugger breakpoints).
- `SC_STOP`: Call `sc_core::sc_stop()` to gracefully end the simulation at the end of the current delta cycle.
- `SC_ABORT`: Call `std::abort()` for an immediate, hard crash.

You apply these overrides using `sc_report_handler::set_actions`.

## Complete Example: Advanced Report Handling

The following example demonstrates overriding actions, catching exceptions from `SC_REPORT_ERROR`, and logging fatal messages to a file. It is a complete, compilable SystemC design.

```cpp
#include <systemc>
#include <iostream>

// A dummy IP block that generates various reports
SC_MODULE(NetworkIP) {
    SC_CTOR(NetworkIP) {
        SC_THREAD(run);
    }

    void run() {
        // 1. A standard info message
        SC_REPORT_INFO("/Company/NetworkIP/Status", "Network IP initialized successfully.");

        wait(10, sc_core::SC_NS);

        // 2. A warning message
        SC_REPORT_WARNING("/Company/NetworkIP/Bandwidth", "Bandwidth utilization exceeding 80%.");

        wait(10, sc_core::SC_NS);

        // 3. An error message. By default, this throws an exception.
        // We will configure the handler in sc_main to NOT throw for this specific msg_type.
        SC_REPORT_ERROR("/Company/NetworkIP/PacketDrop", "Packet dropped due to buffer overflow.");

        wait(10, sc_core::SC_NS);

        // 4. An error message that WILL throw an exception.
        try {
            SC_REPORT_ERROR("/Company/NetworkIP/Checksum", "Invalid packet checksum detected.");
        } catch (const sc_core::sc_report& e) {
            std::cout << "\n[CATCH] Caught a SystemC report exception!\n";
            std::cout << "        Message: " << e.get_msg() << "\n";
            std::cout << "        Type:    " << e.get_msg_type() << "\n";
            std::cout << "        Time:    " << e.get_time() << "\n\n";
        }

        wait(10, sc_core::SC_NS);

        // 5. A fatal message. We configure this in sc_main to NOT abort, but to stop simulation instead.
        SC_REPORT_FATAL("/Company/NetworkIP/HardwareFault", "Thermal limit exceeded! Halting.");

        // This line will not be reached because SC_STOP will end simulation during the next phase.
        SC_REPORT_INFO("/Company/NetworkIP/Status", "This should not print.");
    }
};

int sc_main(int argc, char* argv[]) {
    // ---------------------------------------------------------
    // LRM-Compliant Report Handler Configuration
    // ---------------------------------------------------------

    // Ignore all PacketDrop errors!
    sc_core::sc_report_handler::set_actions("/Company/NetworkIP/PacketDrop", sc_core::SC_DO_NOTHING);

    // Log all warnings to a file, but ALSO display them on stdout
    sc_core::sc_report_handler::set_log_file_name("simulation_warnings.log");
    sc_core::sc_report_handler::set_actions(sc_core::SC_WARNING, sc_core::SC_LOG | sc_core::SC_DISPLAY);

    // Override the fatal action for this specific type to STOP instead of ABORT
    sc_core::sc_report_handler::set_actions(
        "/Company/NetworkIP/HardwareFault",
        sc_core::SC_DISPLAY | sc_core::SC_LOG | sc_core::SC_STOP
    );

    // Instantiate the module
    NetworkIP ip("network_ip_inst");

    std::cout << "Starting simulation...\n";
    sc_core::sc_start();
    std::cout << "Simulation finished at " << sc_core::sc_time_stamp() << "\n";

    return 0;
}
```

### Key Takeaways

1. **Never use `std::cout` for errors:** Always use `SC_REPORT_ERROR` or `SC_REPORT_FATAL`.
2. **Standardize your `msg_type`:** A consistent naming convention makes your IP blocks professional and configurable.
3. **Graceful exits:** Overriding `SC_FATAL` to `SC_STOP` allows your testbench to cleanly extract coverage data before exiting, rather than suffering a hard `abort()`.

## Lesson 93: sc_object, Names, and Hierarchy

Canonical lesson: https://www.learn-systemc.com/tutorials/089-sc-object-names-and-hierarchy

How SystemC builds the object tree, assigns hierarchical names, registers children, and why construction order matters.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# `sc_object`, Names, and Hierarchy

Every visible SystemC component is part of an object tree rooted in the kernel. Modules, ports, exports, primitive channels, processes, and events owned by objects all inherit from or rely on `sc_core::sc_object`.

This matters because hierarchy is not just for pretty printing. It controls default names, full names (`name()`), sensitivity lookup, report context, tracing paths (VCD), CCI parameter paths, and the way tools inspect a model during elaboration and simulation.

### Under the Hood: C++ Implementation in Accellera SystemC

How does the SystemC kernel know who your parent is when you instantiate an `sc_object`? The magic happens inside `sc_simcontext` via a hierarchy stack.

1. **`sc_object_manager`:** The `sc_simcontext` owns an `sc_object_manager` which maintains the master table of every `sc_object` in existence, mapping string names to pointers to guarantee name uniqueness.
2. **The Active Module Stack:** When a module's constructor (`SC_CTOR`) begins execution, the kernel pushes that module onto an internal stack (`hierarchy_push`). When the constructor finishes, it pops it (`hierarchy_pop`).
3. **Implicit Parenting:** When you create an `sc_port` or a nested `sc_module` inside that constructor, the `sc_object` base constructor inspects the top of the `sc_simcontext` hierarchy stack. It automatically registers itself as a child of whatever module is currently on top. This is why you never have to pass `this` to child components to build the tree.

## Standard and source context

## The LRM View on Elaboration and Hierarchy

SystemC operates in phases. The first phase is **elaboration**, where the structural model is built. During elaboration, C++ constructors execute. The IEEE 1666 LRM specifies that the SystemC kernel tracks the current construction context (using the active module). When a new `sc_object` (like a port, signal, or child module) is instantiated, it automatically registers itself as a child of the currently active module.

That is why this pattern works:

```cpp
// Correct hierarchical instantiation
SC_MODULE(Uart) {
    sc_core::sc_in<bool> clk{"clk"}; // Becomes a child of Uart
    sc_core::sc_out<bool> irq{"irq"};

    SC_CTOR(Uart) {
        // SC_METHOD also registers itself as a child process of Uart
        SC_METHOD(tick);
        sensitive << clk.pos();
    }

    void tick() {}
};
```

If `Uart` is instantiated at the top level with the name `"my_uart"`, the ports automatically receive the hierarchical names `"my_uart.clk"` and `"my_uart.irq"`.

## Construction Order and Object Lifetimes

A critical rule mandated by the LRM: **Do structural construction before simulation starts.** Do not create ports, exports, or ordinary modules after elaboration (e.g., inside `end_of_elaboration`, `start_of_simulation`, or process threads) and expect the design to behave correctly.

### The Most Common Lifetime Bug

The most common hierarchy bug is constructing something as a local stack variable inside a constructor:

```cpp
SC_CTOR(Top) {
    Uart uart{"uart"}; // ERROR: Destroyed when constructor returns!
}
```

The object registers itself with the kernel briefly, then C++ destroys it at the end of the scope, leaving a dangling pointer in the kernel's object hierarchy. Always use class members or dynamically allocate (`new`) structural components.

## Explicit vs. Generated Names

Avoid creating important modules or ports with generated names (`sc_core::sc_gen_unique_name`) in production virtual platforms. Stable names become part of:
- CCI configuration parameter paths
- VCD trace paths
- Debug messages
- Waveform bookmarks

## Complete Example: Traversing the Hierarchy

The following complete `sc_main` example demonstrates how to build a valid hierarchy, how object names are assigned, and how to programmatically traverse the `sc_object` tree using standard LRM APIs.

```cpp
#include <systemc>
#include <iostream>
#include <string>

// A simple leaf module
SC_MODULE(Peripheral) {
    sc_core::sc_in<bool> clk{"clk"};

    SC_CTOR(Peripheral) {
        SC_METHOD(logic);
        sensitive << clk.pos();
    }
    void logic() {}
};

// A top-level module containing children
SC_MODULE(SystemTop) {
    sc_core::sc_signal<bool> sys_clk{"sys_clk"};
    Peripheral* uart;
    Peripheral* spi;

    SC_CTOR(SystemTop) {
        // Child objects dynamically allocated. Their lifetimes must
        // persist throughout simulation.
        uart = new Peripheral("uart");
        spi = new Peripheral("spi");

        // Bindings
        uart->clk(sys_clk);
        spi->clk(sys_clk);
    }

    ~SystemTop() {
        delete uart;
        delete spi;
    }
};

// Recursive function to print the object tree
void print_hierarchy(sc_core::sc_object* obj, int depth = 0) {
    if (!obj) return;

    std::string indent(depth * 2, ' ');
    std::cout << indent << "- " << obj->name()
              << " (kind: " << obj->kind() << ")\n";

    // Recursively visit all children
    const std::vector<sc_core::sc_object*>& children = obj->get_child_objects();
    for (auto* child : children) {
        print_hierarchy(child, depth + 1);
    }
}

int sc_main(int argc, char* argv[]) {
    // Instantiate the top-level module
    SystemTop top("top_module");

    // The kernel provides sc_get_top_level_objects() to inspect
    // the root of the hierarchy after elaboration.
    std::cout << "--- SystemC Object Hierarchy ---\n";
    const std::vector<sc_core::sc_object*>& roots = sc_core::sc_get_top_level_objects();
    for (auto* root : roots) {
        print_hierarchy(root);
    }
    std::cout << "--------------------------------\n\n";

    // Start simulation (not strictly necessary here as we just want to see elaboration results)
    sc_core::sc_start(1, sc_core::SC_MS);

    return 0;
}
```

### Explanation of the Output

If you run the above code, you will see output similar to this:

```
--- SystemC Object Hierarchy ---
- top_module (kind: sc_module)
  - top_module.sys_clk (kind: sc_signal)
  - top_module.uart (kind: sc_module)
    - top_module.uart.clk (kind: sc_in)
    - top_module.uart.logic (kind: sc_method_process)
  - top_module.spi (kind: sc_module)
    - top_module.spi.clk (kind: sc_in)
    - top_module.spi.logic (kind: sc_method_process)
--------------------------------
```

Notice how every `sc_object`'s `name()` includes its full hierarchical path. The `kind()` method (from the LRM) returns a string identifying the type of the object, which is very useful for introspection tools and custom tracing setups.

## Source-reading checkpoint

For hierarchy questions, inspect Accellera `sc_module`, `sc_object`, and object-manager code together. Naming is not decoration; it is part of how tools find the model. For bound children, continue into `sc_port_registry` after the object hierarchy is clear.

## Lesson 94: Events and Notification Semantics

Canonical lesson: https://www.learn-systemc.com/tutorials/090-events-and-notification-semantics

Immediate, delta, and timed event notifications, cancellation, event lists, and how the kernel schedules waiting processes.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# Events and Notification Semantics

An `sc_core::sc_event` is the fundamental synchronization primitive in SystemC. It is **not** a stored boolean flag or a queue. It is a scheduling object used by the kernel to make waiting processes runnable at the correct time.

This distinction is heavily emphasized in the IEEE 1666 LRM: **Events have no memory.** If an event is notified before a process explicitly waits on it, the process does not magically remember that notification. The notification is lost.

### Under the Hood: C++ Implementation in Accellera SystemC

To understand why events "have no memory," we look at the Accellera implementation of `sc_event`:

1. **Process Tracking:** An `sc_event` contains lists of processes that are currently sensitive to it (`m_methods_static`, `m_threads_static`, `m_methods_dynamic`, `m_threads_dynamic`).
2. **Notification Dispatch:** When you call `notify()`, the event immediately iterates over these lists. It grabs every `sc_process_b` (thread or method) waiting on it and pushes them directly into the `sc_simcontext`'s runnable queue.
3. **No State Variables:** Notice what is *not* there: an `sc_event` does not contain a boolean `m_triggered` flag. Once the `notify()` function completes its iteration and pushes the waiting processes, its job is done. If a process calls `wait(event)` one microsecond *after* `notify()` was called, the process simply adds itself to the event's dynamic sensitivity list and goes to sleep. Because the event has no memory of the past, the process sleeps forever unless the event is notified *again*.

## Standard and source context

## The Three Forms of Notification

The SystemC LRM models three distinct notification semantics using the overloaded `notify()` method:

1. **Immediate Notification:** `ev.notify()`
   Wakes up sensitive processes in the current evaluation phase. Processes that are suspended on this event are moved to the runnable queue immediately.

2. **Delta Notification:** `ev.notify(sc_core::SC_ZERO_TIME)`
   Schedules the event for the *next delta cycle* at the current simulation time. This is strictly required to model zero-delay hardware semantics, avoiding race conditions and combinational loops.

3. **Timed Notification:** `ev.notify(10, sc_core::SC_NS)`
   Schedules the event to trigger at a specific future simulation time.

### The Notification Override Rule

A core LRM rule specifies what happens when an event is notified multiple times before it actually triggers:
- An immediate notification cancels any pending delta or timed notifications for that event.
- A delta notification cancels any pending timed notifications.
- If multiple timed notifications are requested, the one that occurs *earliest* takes precedence (earlier notifications override later ones).

## Event Lists (AND/OR)

Processes can dynamically wait on combinations of events using event lists:

```cpp
wait(rx_event | timeout_event); // OR list: Resumes when ANY event occurs
wait(a_event & b_event);        // AND list: Resumes when ALL events have occurred
```

An AND list resumes only after all member events have occurred since the wait was armed. If `a_event` fires, the kernel remembers it for this wait expression, and only resumes the thread once `b_event` also fires.

## Cancellation

Pending delta and timed notifications can be actively canceled before they fire:

```cpp
retry_event.cancel();
```
Cancellation is useful for modeling hardware watchdog timers, retries, and interrupt coalescing. (Immediate notifications cannot be canceled because they take effect instantly).

## Complete Example: Notification Semantics and Time

The following complete, compilable `sc_main` demonstrates immediate vs. delta notifications, the lack of memory in events, and how to safely pair events with state variables.

```cpp
#include <systemc>
#include <iostream>
#include <queue>

SC_MODULE(EventDemo) {
    sc_core::sc_event data_ready_evt;
    std::queue<int>   fifo;

    SC_CTOR(EventDemo) {
        SC_THREAD(producer);
        SC_THREAD(consumer);
    }

    void producer() {
        wait(10, sc_core::SC_NS);

        // Push data to state
        std::cout << "@ " << sc_core::sc_time_stamp() << " [Producer] Pushing 42\n";
        fifo.push(42);

        // DELTA notification: Allows consumer to wake up safely in the next
        // delta cycle, preventing read/write race conditions.
        data_ready_evt.notify(sc_core::SC_ZERO_TIME);

        wait(10, sc_core::SC_NS);

        // Immediate notification demonstration.
        std::cout << "@ " << sc_core::sc_time_stamp() << " [Producer] Pushing 99\n";
        fifo.push(99);
        std::cout << "@ " << sc_core::sc_time_stamp() << " [Producer] Immediate Notify...\n";
        data_ready_evt.notify();
        std::cout << "@ " << sc_core::sc_time_stamp() << " [Producer] Finished immediate notify.\n";
    }

    void consumer() {
        while (true) {
            // Wait for the event
            wait(data_ready_evt);

            // Events have no memory! Always check the state (the FIFO)
            // after waking up to see what actually happened.
            while (!fifo.empty()) {
                int val = fifo.front();
                fifo.pop();
                std::cout << "@ " << sc_core::sc_time_stamp()
                          << " [Consumer] Read " << val << " from FIFO.\n";
            }
        }
    }
};

int sc_main(int argc, char* argv[]) {
    EventDemo demo("demo");

    std::cout << "Starting simulation...\n";
    sc_core::sc_start(50, sc_core::SC_NS);
    std::cout << "Simulation finished.\n";

    return 0;
}
```

### Explanation of the Execution

When run, the output looks like this:
```
Starting simulation...
@ 10 ns [Producer] Pushing 42
@ 10 ns [Consumer] Read 42 from FIFO.
@ 20 ns [Producer] Pushing 99
@ 20 ns [Producer] Immediate Notify...
@ 20 ns [Consumer] Read 99 from FIFO.
@ 20 ns [Producer] Finished immediate notify.
Simulation finished.
```

Notice the crucial difference at 20ns: With **immediate notification**, the consumer is awakened and executed *immediately*, preempting the producer. The producer's `"Finished immediate notify."` prints *after* the consumer has finished reading! If a delta notification was used instead, the producer would finish its thread execution entirely for that cycle before the consumer ran.

Always prefer delta notifications when communicating between processes to ensure deterministic evaluation order!

## Lesson 95: Writer Policies and Resolved Signals

Canonical lesson: https://www.learn-systemc.com/tutorials/091-writer-policies-and-resolved-signals

Single-writer checks, many-writer policies, resolved logic, and when sc_signal_rv is the right model.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# Writer Policies and Resolved Signals

Signals look simple until two processes try to drive the same one. The IEEE 1666 LRM strictly regulates how concurrent writes to signals are handled via **writer policies** and **resolved signal types**.

The rule of thumb is simple: if the real hardware has one driver, model it with a single writer policy. If the real hardware has a resolved bus (tri-state, open-drain), model resolution intentionally using resolved signals.

### Under the Hood: C++ Implementation in Accellera SystemC

How does the SystemC kernel efficiently check if multiple processes are driving a signal, and how does resolution logic work?

1. **`sc_writer_policy` Specialization:** The `sc_signal<T, POL>` class is a template. If `POL` is `SC_ONE_WRITER` (the default), the `write()` method caches an `sc_process_b*` pointer to the current process the first time it is called in a delta cycle. On subsequent writes, it simply compares the cached pointer against the active process. If they differ, it throws an `SC_REPORT_ERROR`. If `POL` is `SC_MANY_WRITERS`, this check is optimized out entirely by the compiler via template specialization.
2. **`sc_signal_resolved` Logic Tables:** A resolved signal (`sc_signal_resolved`) does not just store a single value. It maintains a `std::vector<sc_logic>` representing the driving value of *every* connected process. During the kernel's update phase, the `update()` method iterates over this vector and applies a 2D lookup table defined in the LRM to resolve competing drives (e.g., '1' and 'Z' resolves to '1').

## Standard and source context

## Writer Policies (`sc_writer_policy`)

The `sc_core::sc_signal<T, POL>` class template takes a second argument: the writer policy. The LRM defines the `sc_core::sc_writer_policy` enum with the following rules:

1. `SC_ONE_WRITER` (Default): The kernel tracks which process writes to the signal. If a second process attempts to write to the signal during simulation, the kernel instantly throws an `SC_REPORT_ERROR`. This prevents accidental multi-driver bugs that cause non-deterministic behavior depending on process scheduling order.
2. `SC_MANY_WRITERS`: Allows multiple processes to write to the signal in the same delta cycle. However, the last process to write "wins" and overwrites the others. This is non-deterministic unless you explicitly control process execution order!
3. `SC_UNCHECKED_WRITERS`: Disables writer checking entirely (used for performance optimization in legacy models, highly discouraged).

### When to use `SC_MANY_WRITERS`?

Almost never. If a signal has many writers in real hardware, it is usually a multiplexer, an arbiter, or a resolved bus. Use proper structural modeling for muxes/arbiters.

## Resolved Logic (`sc_signal_resolved` and `sc_signal_rv`)

For true hardware bus resolution (tri-state logic), SystemC provides four-state logic data types (`sc_core::sc_logic` and `sc_core::sc_lv<W>`) containing '0', '1', 'Z' (high-impedance), and 'X' (unknown).

When multiple processes drive an `sc_core::sc_signal_resolved` (or `sc_core::sc_signal_rv` for vectors), the kernel applies a strict resolution table during the update phase:
- Driving 'Z' and '1' resolves to '1'.
- Driving '0' and '1' resolves to 'X' (short circuit!).
- Driving 'Z' and 'Z' resolves to 'Z'.

## Complete Example: Single vs. Many vs. Resolved Writers

Here is a complete `sc_main` example demonstrates the default single-writer failure, how `SC_MANY_WRITERS` works, and how `sc_signal_resolved properly models a tri-state bus.

```cpp
#include <systemc>
#include <iostream>

SC_MODULE(BusDemo) {
    // 1. Default: SC_ONE_WRITER (Will error if driven by multiple processes)
    sc_core::sc_signal<bool> single_driver_sig{"single_driver_sig"};

    // 2. SC_MANY_WRITERS: Last writer wins (Dangerous, non-deterministic)
    sc_core::sc_signal<bool, sc_core::SC_MANY_WRITERS> many_driver_sig{"many_driver_sig"};

    // 3. Resolved signal: Hardware-accurate tri-state bus
    sc_core::sc_signal_resolved tri_state_bus{"tri_state_bus"};

    SC_CTOR(BusDemo) {
        SC_METHOD(driver_a);
        SC_METHOD(driver_b);

        SC_METHOD(monitor);
        sensitive << single_driver_sig << many_driver_sig << tri_state_bus;
        dont_initialize();
    }

    void driver_a() {
        // Drive values
        many_driver_sig.write(true);
        tri_state_bus.write(sc_core::SC_LOGIC_1); // Drive HIGH

        // Uncommenting this line and the one in driver_b causes a runtime kernel error!
        // single_driver_sig.write(true);
    }

    void driver_b() {
        // Drive competing values
        many_driver_sig.write(false);
        tri_state_bus.write(sc_core::SC_LOGIC_Z); // Drive High-Impedance (Z)

        // single_driver_sig.write(false);
    }

    void monitor() {
        std::cout << "@ " << sc_core::sc_time_stamp() << "\n"
                  << "  many_driver_sig : " << many_driver_sig.read() << " (Last writer won)\n"
                  << "  tri_state_bus   : " << tri_state_bus.read() << " (1 and Z resolved to 1)\n";
    }
};

int sc_main(int argc, char* argv[]) {
    BusDemo demo("demo");

    std::cout << "Starting simulation...\n";
    sc_core::sc_start(10, sc_core::SC_NS);

    return 0;
}
```

### Explanation of the Execution

When run, the output shows:

```
Starting simulation...
@ 0 s
  many_driver_sig : 0 (Last writer won)
  tri_state_bus   : 1 (1 and Z resolved to 1)
```

In `many_driver_sig`, `driver_b` executed after `driver_a` (due to kernel scheduling), so it overwrote `true` with `false`. This is a classic race condition.

In `tri_state_bus`, regardless of scheduling order, the kernel's update phase resolved `SC_LOGIC_1` and `SC_LOGIC_Z` correctly to `SC_LOGIC_1`, perfectly modeling a hardware bus where a driver asserts '1' while another driver disconnects ('Z').

## Lesson 96: Datatype Performance and Correctness

Canonical lesson: https://www.learn-systemc.com/tutorials/092-datatype-performance-and-correctness

Choosing between C++ types, SystemC integer types, bit vectors, logic vectors, fixed-point types, and TLM byte arrays.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# Datatype Performance and Correctness

SystemC provides an extensive library of custom datatypes because hardware modeling requires precise bit widths, four-state logic, and fixed-point arithmetic. However, a common beginner trap is using the most "hardware-looking" type everywhere. This drastically reduces simulation performance and makes the C++ code cumbersome to read.

The IEEE 1666 LRM strictly defines these datatypes. Knowing when to use native C++ types versus SystemC types is a hallmark of an expert SystemC architect.

### Under the Hood: C++ Implementation in Accellera SystemC

How are SystemC datatypes implemented, and why are some slower than others?

1. **`sc_int<W>` and `sc_uint<W>` (Fast):** In the Accellera source code, these are implemented via C++ template metaprogramming. An `sc_uint<32>` holds a single underlying native `uint64_t` data member. All operators (`+`, `-`, `&`) are heavily inlined and simply mask the upper bits to enforce the width `W`. This allows the C++ compiler to optimize them almost to the speed of native integers.
2. **`sc_bigint<W>` (Slow):** If `W > 64`, SystemC dynamically allocates an array of 32-bit `unsigned int` words to hold the value. Basic arithmetic now requires a `for` loop across multiple words, mimicking a software big-number library.
3. **`sc_lv<W>` (Very Slow):** A logic vector does not store bits. It stores an array of `sc_logic` objects (representing 0, 1, Z, X). Every logical operation requires evaluating the LRM 4-state resolution tables for *every single element* in the array.
4. **Proxy Classes (`sc_subref`):** A major performance pitfall is the use of proxy classes for bit-selection (`[]`) and part-selection (`range()`). When you write `reg.range(15, 8)`, SystemC returns a temporary proxy object (`sc_dt::sc_subref`). If you nest these deeply, the C++ compiler generates massive amounts of temporary proxy objects and virtual method calls, severely degrading simulation speed.

## Standard and source context

## The LRM Datatype Categories

The standard defines several datatype groups under the `sc_dt` namespace:

1. **Native C++ Types:** (`int`, `uint32_t`, `bool`)
   *Performance:* Maximum.
   *Use Case:* Virtual Platform (TLM) internal state, counters, flags, memory arrays.
2. **Limited-Precision Fixed-Width Integers:** (`sc_dt::sc_int<W>`, `sc_dt::sc_uint<W>`)
   *Performance:* High (implemented using 64-bit native integers under the hood). Valid for $W \le 64$.
   *Use Case:* Register fields, exact small hardware width arithmetic.
3. **Arbitrary-Precision Integers:** (`sc_dt::sc_bigint<W>`, `sc_dt::sc_biguint<W>`)
   *Performance:* Slow (dynamically allocates arrays of words). Valid for $W > 64$.
   *Use Case:* Cryptographic keys, very wide buses, wide memory payloads.
4. **Bit and Logic Vectors:** (`sc_dt::sc_bv<W>`, `sc_dt::sc_lv<W>`)
   *Performance:* Very Slow (uses proxy objects, stores bit arrays, resolves 4-state logic for `sc_lv`).
   *Use Case:* Pin-level RTL interfaces, unknown ('X') or high impedance ('Z') states.
5. **Fixed-Point Types:** (`sc_dt::sc_fixed`, `sc_dt::sc_ufixed`)
   *Performance:* Moderate to Slow (handles quantization and overflow).
   *Use Case:* DSP algorithms, AMS (Analog Mixed Signal) boundaries.

## The Proxy Object Problem

A major performance pitfall in SystemC datatypes is the use of **proxy classes** for bit-selection (`[]`) and part-selection (`range()`).

When you write `reg.range(15, 8)`, SystemC does not return an integer. It returns a temporary proxy object (`sc_dt::sc_subref`). If you nest these deeply, the C++ compiler generates massive amounts of temporary proxy objects, severely degrading simulation speed.

**Best Practice:** Convert to native C++ types for complex arithmetic, then assign back to SystemC types only at the module boundaries.

## Complete Example: Datatype Trade-offs

Here is a complete `sc_main` example demonstrates how to correctly mix native C++ types with SystemC limited-precision integers, and how to use part-select proxies safely.

```cpp
#include <systemc>
#include <iostream>
#include <iomanip>

SC_MODULE(DatatypeDemo) {
    // Port using exact-width hardware type
    sc_core::sc_in<sc_dt::sc_uint<12>> address_in{"address_in"};

    // Internal state using fast native C++ type (Best Practice for VPs)
    uint32_t internal_memory[4096];

    // Hardware-accurate register representing a 32-bit control register
    sc_dt::sc_uint<32> control_reg;

    SC_CTOR(DatatypeDemo) {
        SC_METHOD(process_transaction);
        sensitive << address_in;
        dont_initialize();

        // Initialize memory
        for (int i = 0; i < 4096; i++) internal_memory[i] = 0;
        control_reg = 0;
    }

    void process_transaction() {
        // 1. Read from SystemC type to native C++ type (Fast)
        uint32_t addr = address_in.read();

        // 2. Perform operations using native C++ (Fast)
        if (addr < 4096) {
            internal_memory[addr] = 0xDEADBEEF;
        }

        // 3. Using SystemC Proxy Objects (range) correctly
        // Extracting bits [11:8] as a 4-bit unsigned integer
        sc_dt::sc_uint<4> page = address_in.read().range(11, 8);

        // Packing bits into the control register
        // Avoid deep nesting: reg.range() = (a, b);
        control_reg.range(3, 0) = page;
        control_reg.range(31, 28) = 0xF;

        std::cout << "@ " << sc_core::sc_time_stamp()
                  << " Addr: 0x" << std::hex << addr
                  << " Page: 0x" << page
                  << " Control Reg: 0x" << control_reg << "\n";
    }
};

// Testbench to drive the module
SC_MODULE(Testbench) {
    sc_core::sc_signal<sc_dt::sc_uint<12>> addr_sig{"addr_sig"};
    DatatypeDemo* demo;

    SC_CTOR(Testbench) {
        demo = new DatatypeDemo("demo_inst");
        demo->address_in(addr_sig);

        SC_THREAD(drive);
    }

    void drive() {
        wait(10, sc_core::SC_NS);
        addr_sig.write(0x0A4); // Write 12-bit value

        wait(10, sc_core::SC_NS);
        addr_sig.write(0xF00);
    }

    ~Testbench() {
        delete demo;
    }
};

int sc_main(int argc, char* argv[]) {
    Testbench tb("tb");

    std::cout << "Starting simulation...\n";
    sc_core::sc_start(50, sc_core::SC_NS);

    return 0;
}
```

### Explanation of the Execution

When run, the output shows:

```
Starting simulation...
@ 10 ns Addr: 0xa4 Page: 0x0 Control Reg: 0xf0000000
@ 20 ns Addr: 0xf00 Page: 0xf Control Reg: 0xf000000f
```

Notice how `address_in.read().range(11, 8)` correctly extracts the top 4 bits of the 12-bit address. When driving `0xF00`, the top nibble is `F`, which is packed into the lowest 4 bits of the 32-bit `control_reg`.

Using `uint32_t` for the `internal_memory` ensures that the simulation runs at native C++ speeds for the bulk of the data storage, while `sc_dt::sc_uint` is reserved for explicit hardware boundaries.

## Source-reading checkpoint

When datatype choices affect a signal boundary, inspect the Accellera `sc_writer_policy` checks as well as the datatype implementation. Correct width does not replace correct ownership.

## Lesson 97: Report Handler Internals

Canonical lesson: https://www.learn-systemc.com/tutorials/093-report-handler-internals

How SC_REPORT macros become sc_report objects, actions, severity counts, cached reports, and simulation control.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# Report Handler Internals

Reports are the diagnostic backbone of a serious SystemC model. `std::cout` is fine for quick hacks, but a reusable, LRM-compliant model must use `SC_REPORT_INFO`, `SC_REPORT_WARNING`, `SC_REPORT_ERROR`, and `SC_REPORT_FATAL`.

To get genuinely comfortable with SystemC reporting, you need to understand how the `sc_core::sc_report_handler` translates a macro call into an actionable event, how severity limits work, and how to write custom report handlers.

### Under the Hood: C++ Implementation in Accellera SystemC

What happens inside the Accellera `sc_report_handler` when you register a custom hook?
1. **The Core Pointer:** `sc_report_handler` contains a static function pointer `sc_report_handler_proc handler`. By default, this points to `sc_def_report_handler`, which handles the standard terminal printing.
2. **Hooking:** When you call `sc_report_handler::set_handler()`, you are overriding this static pointer. From that moment on, whenever any macro calls `report()`, your custom C++ function receives the fully constructed `sc_report` object.
3. **The `sc_report` Object:** The `sc_report` class encapsulates the diagnostic message. It is designed to be exception-safe. When `SC_THROW` is resolved, the kernel uses `throw *this;` inside the `sc_report` context, propagating the C++ exception up the call stack until it is caught by a `catch (const sc_core::sc_report& e)` block in the user testbench or reaches the `sc_simcontext` boundaries (which then aborts).

## Standard and source context

## The Standard LRM Report Flow

When you call `SC_REPORT_WARNING("ROUTER", "Unmapped access");`, the LRM dictates the following sequence:

1. **Macro Expansion:** The macro captures the file name (`__FILE__`) and line number (`__LINE__`).
2. **Object Creation:** It instantiates an `sc_core::sc_report` object containing the severity, message type ("ROUTER"), message text, file, and line.
3. **Handler Dispatch:** The global `sc_core::sc_report_handler::report()` function is invoked.
4. **Action Resolution:** The handler looks up the configured `sc_action` for the specific message type or severity.
5. **Limit Checking:** The handler increments the count for that severity/type. If the count exceeds the configured limit (e.g., maximum 10 errors allowed), the handler forces an `SC_STOP` or `SC_ABORT`.
6. **Execution:** The resolved actions (`SC_LOG`, `SC_DISPLAY`, `SC_THROW`, etc.) are executed.

## Verbosity Limits

SystemC also supports verbosity levels (from `SC_NONE` to `SC_DEBUG`). You can filter out low-level debug messages dynamically without recompiling:

```cpp
sc_core::sc_report_handler::set_verbosity_level(sc_core::SC_HIGH);
// This will be suppressed if verbosity is set below SC_DEBUG
SC_REPORT_INFO_VERB("CACHE", "Cache hit on line 4", sc_core::SC_DEBUG);
```

## Severity Limits and Quotas

You can instruct the kernel to automatically stop the simulation if too many warnings or errors occur:

```cpp
// Stop simulation after 5 errors
sc_core::sc_report_handler::set_actions(sc_core::SC_ERROR, sc_core::SC_DISPLAY | sc_core::SC_STOP);
sc_core::sc_report_handler::stop_after(sc_core::SC_ERROR, 5);
```

## Complete Example: Custom Report Handler

The ultimate power of the `sc_report_handler` is the ability to bypass the default printing mechanism entirely and install a **custom handler hook**. This is heavily used in professional verification environments (like UVM-SystemC) to pipe SystemC reports into external logging frameworks (like JSON loggers or Python test runners).

Here is a complete `sc_main` demonstrates setting up a custom report handler.

```cpp
#include <systemc>
#include <iostream>
#include <string>

// 1. Define a custom handler function matching the LRM signature
void custom_json_report_handler(const sc_core::sc_report& rep, const sc_core::sc_actions& actions) {
    // Only process if the action dictates display or log
    if (actions & sc_core::SC_DISPLAY) {
        std::cout << "{ \"severity\": \"";
        switch (rep.get_severity()) {
            case sc_core::SC_INFO:    std::cout << "INFO"; break;
            case sc_core::SC_WARNING: std::cout << "WARNING"; break;
            case sc_core::SC_ERROR:   std::cout << "ERROR"; break;
            case sc_core::SC_FATAL:   std::cout << "FATAL"; break;
        }
        std::cout << "\", \"type\": \"" << rep.get_msg_type() << "\""
                  << ", \"time\": \"" << rep.get_time() << "\""
                  << ", \"file\": \"" << rep.get_file_name() << "\""
                  << ", \"line\": " << rep.get_line_number()
                  << ", \"message\": \"" << rep.get_msg() << "\" }\n";
    }

    // Always respect the SC_STOP and SC_ABORT actions in a custom handler!
    if (actions & sc_core::SC_STOP) {
        sc_core::sc_stop();
    }
    if (actions & sc_core::SC_ABORT) {
        std::abort();
    }
    if (actions & sc_core::SC_THROW) {
        throw rep;
    }
}

SC_MODULE(CustomLoggerDemo) {
    SC_CTOR(CustomLoggerDemo) {
        SC_THREAD(run);
    }

    void run() {
        wait(10, sc_core::SC_NS);
        SC_REPORT_INFO("SYSTEM", "Booting kernel...");

        wait(15, sc_core::SC_NS);
        SC_REPORT_WARNING("MEM", "Memory usage at 90%");

        wait(5, sc_core::SC_NS);
        SC_REPORT_ERROR("BUS", "Timeout on AHB bus transaction!");
    }
};

int sc_main(int argc, char* argv[]) {
    // 2. Install the custom handler hook
    sc_core::sc_report_handler::set_handler(custom_json_report_handler);

    // Ensure errors don't throw, but just display (which routes to our JSON handler)
    sc_core::sc_report_handler::set_actions(sc_core::SC_ERROR, sc_core::SC_DISPLAY);

    CustomLoggerDemo demo("demo");

    sc_core::sc_start();

    return 0;
}
```

### Explanation of the Execution

When run, instead of the standard SystemC text output, the custom handler generates structured JSON logs:

```json
{ "severity": "INFO", "type": "SYSTEM", "time": "10 ns", "file": "main.cpp", "line": 42, "message": "Booting kernel..." }
{ "severity": "WARNING", "type": "MEM", "time": "25 ns", "file": "main.cpp", "line": 45, "message": "Memory usage at 90%" }
{ "severity": "ERROR", "type": "BUS", "time": "30 ns", "file": "main.cpp", "line": 48, "message": "Timeout on AHB bus transaction!" }
```

By hooking into `sc_report_handler`, you have complete control over how diagnostics are formatted and routed, allowing seamless integration with CI/CD pipelines and external databases.

## Lesson 98: Kernel Source Map

Canonical lesson: https://www.learn-systemc.com/tutorials/094-kernel-source-map

A guided map through the Accellera SystemC kernel source for modules, processes, events, signals, ports, reports, and TLM.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# Kernel Source Map

Reading the Accellera SystemC Proof-of-Concept (PoC) source code is easier when you know which files map to which IEEE 1666 LRM concepts. This page is a map. It is not a replacement for the LRM, but it bridges the gap between theoretical standards and actual C++ implementation.

When a simulator behaves unexpectedly, stepping through these specific files in GDB or Visual Studio is the fastest way to understand the kernel's rules. We will aggressively explore how the Accellera kernel implements these features under the hood.

## Standard and source context

## Core Simulation Context (`sc_simcontext`)

**LRM Reference:** Section 4 (Elaboration and Simulation Semantics)
**Source Files:**
- `sysc/kernel/sc_simcontext.h`
- `sysc/kernel/sc_simcontext.cpp`

The simulation context (`sc_simcontext`) is the beating heart of SystemC, accessible via the singleton getter `sc_get_curr_simcontext()`.

Under the hood, `sc_simcontext` maintains several crucial data structures:
- `sc_runnable m_runnable`: The run queues for processes ready to execute in the current delta cycle. It holds separated queues for `SC_METHOD`s and `SC_THREAD`s.
- `std::vector<sc_update_action*> m_update_list`: Channels that have called `request_update()` during the evaluation phase are pushed here.
- `sc_pq<sc_event_timed*> m_timed_events`: A priority queue of events scheduled for future simulation times.
- `sc_time m_curr_time`: The current simulation timestamp (`sc_time_stamp()`).

The core scheduling algorithm is implemented in `sc_simcontext::crunch()`. It loops through the `m_runnable` processes (Evaluation Phase), then processes the `m_update_list` by invoking virtual `update()` methods on those primitives (Update Phase), and finally triggers delta notifications, looping until the delta cycle stabilizes.

## Modules and Object Hierarchy

**LRM Reference:** Section 5 (Module and Hierarchy)
**Source Files:**
- `sysc/kernel/sc_object.*`
- `sysc/kernel/sc_module.*`
- `sysc/kernel/sc_module_name.*`
- `sysc/kernel/sc_object_manager.*`

Every structural element in SystemC derives from `sc_object`. Hierarchy is dynamically constructed during C++ instantiation, driven by the `sc_module_name` class.

When you pass a string to an `sc_module` constructor, you implicitly instantiate an `sc_module_name`. Its constructor pushes a pointer to itself onto a static stack inside the `sc_object_manager` of the simulation context. When the child `sc_object` (e.g., a port or a sub-module) is constructed inside that scope, it looks at the top of this stack to discover its parent `sc_module`.

Once the constructor finishes, the `sc_module_name` destructor pops itself off the stack. This elegant RAII-based scoping avoids the need for users to manually pass `parent` pointers to every object.

## Processes and Scheduling

**LRM Reference:** Section 5.2 (Processes)
**Source Files:**
- `sysc/kernel/sc_process.*`
- `sysc/kernel/sc_method_process.*`
- `sysc/kernel/sc_thread_process.*`
- `sysc/kernel/sc_coros.*` (Coroutines)

A SystemC process is a C++ object. `sc_method_process` and `sc_thread_process` inherit from a common `sc_process_b` base class, which stores the dynamic sensitivity list (events the process is waiting for).

Under the hood, `SC_THREAD`s require actual stack context switching, bypassing the OS thread scheduler. The Accellera PoC uses a coroutine library, traditionally QuickThreads (`qt`) or standard POSIX threads (`pthreads`) configured via macros. When you call `wait()`, the `sc_thread_process` saves its CPU registers to its allocated stack (via assembly-level context switches like `qt_block`), and yields control back to `sc_simcontext::crunch()`, which loads the CPU registers of the next runnable process. `SC_METHOD`s, however, are just standard C++ function pointers executed synchronously, saving memory by not requiring their own call stack.

## Events and Time

**LRM Reference:** Section 5.10 (sc_event) and Section 5.11 (sc_time)
**Source Files:**
- `sysc/kernel/sc_event.*`
- `sysc/kernel/sc_time.*`

An `sc_event` is essentially a notification node. Calling `notify(SC_ZERO_TIME)` adds the event to `sc_simcontext::m_delta_events`. Calling `notify(10, SC_NS)` wraps the notification into an `sc_event_timed` object and inserts it into the `m_timed_events` priority queue.

Internally, `sc_time` is represented as an unsigned 64-bit integer (`sc_dt::uint64`). The absolute integer value represents time accurately scaled against the selected global time resolution (e.g., femtoseconds), preventing floating-point rounding errors across the simulation. The global resolution is stored statically in the `sc_time_params` struct.

## Signals and Primitive Channels

**LRM Reference:** Section 6 (Predefined Channels)
**Source Files:**
- `sysc/communication/sc_prim_channel.*`
- `sysc/communication/sc_signal.*`

When a process writes to an `sc_signal` via `operator=()`, the signal does not immediately change its value. Instead, `sc_signal::write()` stores the new value in a temporary `m_new_val` variable.

If `m_new_val` differs from `m_cur_val`, the signal calls `request_update()`. The base class `sc_prim_channel` then registers `this` pointer into the simulation context's `m_update_list`. Later, during the Update Phase, the kernel traverses `m_update_list` and calls the pure virtual `update()` method. The `sc_signal`'s implementation of `update()` commits `m_new_val` to `m_cur_val` and notifies its internal `m_value_changed_event`.

This delayed assignment ensures determinism and prevents combinational race conditions regardless of the order in which concurrent processes execute.

## Complete Example: Accessing Kernel Internals

While you shouldn't rely on implementation-specific APIs, the LRM does guarantee certain kernel introspection APIs. Here is a complete `sc_main` demonstrates querying the simulation context and object hierarchy.

```cpp
#include <systemc>
#include <iostream>
#include <vector>

SC_MODULE(KernelMapDemo) {
    SC_CTOR(KernelMapDemo) {
        SC_THREAD(run);
    }

    void run() {
        wait(10, sc_core::SC_NS);

        // LRM API to check simulation status
        std::cout << "[Kernel] Current Time: " << sc_core::sc_time_stamp() << "\n";
        std::cout << "[Kernel] Delta Count: " << sc_core::sc_delta_count() << "\n";

        // LRM API to query the object hierarchy
        std::cout << "[Kernel] Module Name: " << this->name() << "\n";
        std::cout << "[Kernel] Object Kind: " << this->kind() << "\n";

        sc_core::sc_stop();
    }
};

int sc_main(int argc, char* argv[]) {
    KernelMapDemo demo("kernel_demo");

    // LRM API to check engine status
    std::cout << "Engine status before start: ";
    if (sc_core::sc_get_status() == sc_core::SC_ELABORATION) {
        std::cout << "ELABORATION\n";
    }

    sc_core::sc_start();

    std::cout << "Engine status after stop: ";
    if (sc_core::sc_get_status() == sc_core::SC_STOPPED) {
        std::cout << "STOPPED\n";
    }

    return 0;
}
```

### Explanation of Execution

```
Engine status before start: ELABORATION
[Kernel] Current Time: 10 ns
[Kernel] Delta Count: 1
[Kernel] Module Name: kernel_demo
[Kernel] Object Kind: sc_module
Engine status after stop: STOPPED
```

By tracing through `sc_simcontext.cpp`, you can see exactly how `sc_get_status()` updates its internal state machine from `SC_ELABORATION` -> `SC_RUNNING` -> `SC_STOPPED`. Understanding this source map allows you to debug complex deadlock or ordering issues rapidly.

## Lesson 99: Deep Dive: sc_vector Internals and Dynamic Assembly

Canonical lesson: https://www.learn-systemc.com/tutorials/095-deep-dive-sc-vector-internals-and-dynamic-assembly

An exhaustive look into the IEEE 1666-2023 rules and Accellera source code implementation for sc_vector, custom creators, and dynamic port assembly.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# Deep Dive: sc_vector Internals and Dynamic Assembly

When designing large SystemC architecturesâ€”like multi-core processors, Network-on-Chip (NoC) routers, or massive memory banksâ€”manually declaring and binding individual ports and submodules becomes unmanageable. To solve this, the IEEE 1666 standard defines `sc_vector` (Section 8.5 of the LRM), a dedicated container for managing collections of SystemC objects (`sc_object`).

Unlike `std::vector`, `sc_vector` is designed explicitly to participate in the SystemC elaboration hierarchy. Let's dig into the Accellera kernel source code to understand how `sc_vector` manages naming, `sc_object_manager` scopes, and dynamic assembly.

## Standard and source context

## The Problem with `std::vector` in SystemC

You might wonder: *Why not just use `std::vector<sc_in<int>>`?*

SystemC relies on the **object hierarchy** (the `sc_object` tree) built during elaboration. When you create an `sc_object` (like a port or module), its constructor pushes an `sc_module_name` object onto a static stack inside the `sc_object_manager`. If you create a `std::vector` of ports, the standard library allocates them contiguously in memory via `new[]`. The C++ default constructor is invoked, passing no names, which completely bypasses the `sc_module_name` stack. Your ports end up unnamed and orphans in the kernel hierarchy.

`sc_vector` solves this by:
1. Deriving from `sc_object` itself, becoming a valid node in the hierarchy.
2. Enforcing that it is populated via an `init()` method rather than C++ standard allocators.
3. Dynamically generating names (`my_vector_name_0`, `my_vector_name_1`) and explicitly constructing `sc_module_name` objects for each element.

## Source Code Mechanics: How `sc_vector` Works

If you peek into the Accellera SystemC repository (`sysc/utils/sc_vector.h`), you'll notice that `sc_vector<T>` inherits from `sc_vector_base`. It acts as a wrapper around an internal `std::vector<void*>`.

When `.init(size)` is called, the kernel executes a loop. For each index `i`:
1. It uses `sprintf` or `std::string` concatenation to append `"_"` and `i` to the vector's name.
2. It invokes a **creator** function.
3. The creator invokes the `T` constructor, taking the generated string and casting it into an `sc_module_name`.
4. The pointer to the created object is pushed into the underlying `std::vector`.

By default, it uses a standard creator that just passes the name. However, if your module requires additional constructor arguments (like an ID, a configuration object, or a memory map), you must use a **custom creator**.

A custom creator is a function object (or a lambda in modern C++) that takes two arguments:
1. `const char* name`: The generated name for the element.
2. `size_t index`: The index of the element being created.

## End-to-End Example: Vector Assembly and Custom Creators

The following code demonstrates an exhaustive, 100% compilable example of:
1. Creating a custom module (`Worker`) that requires an `id` in its constructor.
2. Using a lambda function as a custom creator for `sc_vector<Worker>`.
3. Assembling and binding vectors of ports to vectors of signals.

```cpp
#include <systemc>
#include <iostream>
#include <vector>

// -------------------------------------------------------------------------
// 1. A custom module requiring arguments beyond just a name
// -------------------------------------------------------------------------
class Worker : public sc_core::sc_module {
public:
    sc_core::sc_in<bool> clk;
    sc_core::sc_in<int>  data_in;
    sc_core::sc_out<int> data_out;

    int worker_id;

    // Notice the constructor takes an ID alongside the name
    Worker(sc_core::sc_module_name name, int id)
        : sc_core::sc_module(name), worker_id(id)
    {
        SC_METHOD(process_data);
        sensitive << clk.pos();
        dont_initialize();
    }

    void process_data() {
        int val = data_in.read();
        // Simple operation: multiply input by worker ID
        data_out.write(val * worker_id);
        std::cout << "@" << sc_core::sc_time_stamp() << " "
                  << name() << " processed data: " << val
                  << " -> " << (val * worker_id) << std::endl;
    }
};

// -------------------------------------------------------------------------
// 2. The Top-level module managing vectors
// -------------------------------------------------------------------------
class Top : public sc_core::sc_module {
public:
    sc_core::sc_in<bool> clk;

    // Vectors of signals
    sc_core::sc_vector<sc_core::sc_signal<int>> sig_in;
    sc_core::sc_vector<sc_core::sc_signal<int>> sig_out;

    // Vector of submodules
    sc_core::sc_vector<Worker> workers;

    SC_HAS_PROCESS(Top);

    Top(sc_core::sc_module_name name, size_t num_workers)
        : sc_core::sc_module(name),
          // Initialize signal vectors with default creators
          sig_in("sig_in", num_workers),
          sig_out("sig_out", num_workers),
          // Defer initialization of workers (no size passed here)
          workers("workers")
    {
        // Use a lambda as a custom creator for the workers vector
        // The LRM specifies the signature: T* creator(const char* name, size_t index)
        auto worker_creator = [](const char* n, size_t i) -> Worker* {
            // We pass the index 'i' + 1 as the worker ID
            // The string 'n' is safely cast to an sc_module_name inside Worker
            return new Worker(n, i + 1);
        };

        // Initialize the workers vector with the size and the custom creator
        workers.init(num_workers, worker_creator);

        // Dynamic Assembly (Binding)
        // sc_vector allows binding a vector of ports to a vector of signals
        // using the sc_assemble_vector utility or manual iteration.
        for (size_t i = 0; i < num_workers; ++i) {
            workers[i].clk(clk);
            workers[i].data_in(sig_in[i]);
            workers[i].data_out(sig_out[i]);
        }

        SC_THREAD(stimulus);
    }

    void stimulus() {
        for (int i = 0; i < 3; ++i) {
            // Write distinct data to each worker's input signal
            for (size_t w = 0; w < workers.size(); ++w) {
                sig_in[w].write((i + 1) * 10);
            }
            wait(10, sc_core::SC_NS); // Wait for processing
        }
        sc_core::sc_stop();
    }
};

// -------------------------------------------------------------------------
// 3. sc_main execution
// -------------------------------------------------------------------------
int sc_main(int argc, char* argv[]) {
    // Suppress Info messages to keep output clean
    sc_core::sc_report_handler::set_actions("/IEEE_Std_1666/deprecated", sc_core::SC_DO_NOTHING);

    sc_core::sc_clock clock("clock", 10, sc_core::SC_NS);

    Top top("top_module", 4); // Instantiate with 4 workers
    top.clk(clock);

    std::cout << "Starting simulation with sc_vector..." << std::endl;
    sc_core::sc_start();
    std::cout << "Simulation finished." << std::endl;

    return 0;
}
```

## `sc_assemble_vector` Utility

In the example above, we bound ports manually using a `for` loop. The Accellera source provides `sc_assemble_vector`, which uses an internal `sc_vector_iter` and pointer-to-member iterators to extract ports from child modules en masse.

If you have a vector of submodules, and you want to extract a specific port from all of them into a new vector of ports (or bind them directly to a vector of signals), `sc_assemble_vector` avoids manual loops.

```cpp
// Example of using sc_assemble_vector (pseudo-code)
sc_core::sc_vector<sc_core::sc_in<int>> aggregated_ports("aggregated_ports");

// Extract the 'data_in' port from every worker and aggregate them
// Under the hood, this iterates over workers[i].*&Worker::data_in
sc_core::sc_assemble_vector(workers, &Worker::data_in).bind(aggregated_ports);

// Now you can bind the aggregated ports directly to a signal vector!
aggregated_ports.bind(sig_in);
```
*Note: Due to compiler variations in pointer-to-member resolution, explicitly looping as shown in the complete example is often considered the safest and most readable fallback in production IP.*

## Source-reading checkpoint

For dynamic assembly, inspect Accellera `sc_vector` beside `sc_module` hierarchy code. The useful question is when the vector creates objects and when binding becomes final. When vectors contain ports, follow final registration through `sc_port_registry`.

## Lesson 100: Deep Dive: Fixed-Point Datatypes (sc_fixed) and Quantization

Canonical lesson: https://www.learn-systemc.com/tutorials/096-deep-dive-fixed-point-datatypes-sc-fixed-and-quant

Master the IEEE 1666-2023 fixed-point types, exploring quantization modes, overflow mechanics, and the underlying math of sc_fixed and sc_ufixed.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# Deep Dive: Fixed-Point Datatypes (`sc_fixed`) and Quantization

In hardware design, floating-point arithmetic (like C++ `float` or `double`) is typically avoided due to massive silicon area, high power consumption, and long propagation delays. Instead, designers rely on **Fixed-Point Arithmetic**. The IEEE 1666 standard provides dedicated classes for this: `sc_fixed` (signed) and `sc_ufixed` (unsigned), along with their fast equivalents `sc_fixed_fast` and `sc_ufixed_fast`.

This tutorial breaks down the anatomy of a fixed-point number, explores the detailed quantization (`sc_q_mode`) and overflow (`sc_o_mode`) mechanics, and examines the Accellera source code to understand the performance overhead of these types.

## Standard and source context

## Anatomy of a Fixed-Point Type

To use a fixed-point type, you must define its geometry:

```cpp
sc_fixed<wl, iwl, q_mode, o_mode, n_bits>
```

- **`wl` (Word Length):** Total number of bits.
- **`iwl` (Integer Word Length):** Number of bits located to the left of the binary point (including the sign bit for signed types).
- **`q_mode` (Quantization Mode):** How to handle bits that are discarded on the right (fractional bits) when casting to a type with fewer fractional bits.
- **`o_mode` (Overflow Mode):** How to handle bits that overflow on the left (integer bits) when casting to a type with fewer integer bits.
- **`n_bits`:** Number of saturated bits (only relevant for certain overflow modes).

The number of fractional bits is simply `wl - iwl`. Note that `iwl` can be greater than `wl` (implying trailing zeros) or negative (implying leading fractional zeros), though typical use cases have `0 < iwl <= wl`.

## Source Code Mechanics: `sc_fxnum` vs `sc_fxval`

If you read the Accellera source code in `sysc/datatypes/fx/`, you will see that `sc_fixed` is merely a template wrapper around the base class `sc_fxnum`.

When you perform arithmetic on an `sc_fixed`, the kernel does not operate directly on the bit array. Instead, the operands are converted into an intermediate representation called `sc_fxval`.
- `sc_fxval` dynamically allocates an array of 32-bit words (`m_rep`) to hold an arbitrary-precision mantissa, along with an exponent.
- The arithmetic operation (addition, multiplication) is performed in this high-precision `sc_fxval` space.
- The result is then cast back into the target `sc_fxnum`. During this cast, the target's `sc_fxtype_params` are applied, which triggers the Quantization (`sc_q_mode`) and Overflow (`sc_o_mode`) logic.

Furthermore, every `sc_fxnum` contains a pointer to an `sc_fxnum_observer`. This is a design pattern used to notify VCD waveform tracers whenever the fixed-point value changes, which adds memory overhead to every single fixed-point variable.

## Quantization Modes (`sc_q_mode`)

When you assign a highly precise number to a less precise fixed-point variable, you lose fractional bits. Quantization defines *how* that loss is handled in the `sc_fxval` to `sc_fxnum` cast:

1. **`SC_TRN` (Truncation):** Default. Simply chops off the extra bits. This approaches negative infinity.
2. **`SC_TRN_ZERO`:** Truncates towards zero.
3. **`SC_RND` (Round to positive infinity):** Adds 0.5 to the LSB being kept, carrying over if needed.
4. **`SC_RND_ZERO`:** Rounds towards zero.
5. **`SC_RND_MIN_INF`:** Rounds towards negative infinity.
6. **`SC_RND_INF`:** Rounds away from zero.
7. **`SC_RND_CONV` (Convergent Rounding / Banker's Rounding):** Rounds to the nearest even number if exactly halfway. Minimizes statistical bias in DSP algorithms.

## Overflow Modes (`sc_o_mode`)

When an assignment exceeds the maximum representable value (integer bits are lost), overflow handling determines the outcome:

1. **`SC_WRAP` (Wrap-around):** Default. The bits simply roll over, ignoring the lost MSBs.
2. **`SC_WRAP_SM`:** Wrap-around with Sign Magnitude representation.
3. **`SC_SAT` (Saturation):** Clips to the maximum positive or negative representable value. Crucial for DSP (e.g., audio doesn't flip from loud positive to loud negative, it just distorts softly).
4. **`SC_SAT_ZERO`:** Clips to zero on overflow.
5. **`SC_SAT_SYM`:** Symmetrical saturation. (e.g., if max is 7, min is -7 instead of -8).

## The Fast Types (`sc_fixed_fast`)

The standard types (`sc_fixed`) use arbitrary-precision arithmetic internally (`sc_fxval`), which relies on heap allocations and loops.
If your `wl` is less than or equal to 53 bits (the mantissa size of a standard `double`), you should use `sc_fixed_fast` and `sc_ufixed_fast`.

In the Accellera kernel, `sc_fixed_fast` derives from `sc_fxnum_fast`. Arithmetic operations convert operands to `sc_fxval_fast`, which is backed directly by a native C++ `double` (`m_val`). This bypasses all array allocations, delegating the math directly to your CPU's FPU, resulting in massive simulation speedups while retaining bit-accurate semantics during the final assignment cast.

## End-to-End Example: DSP Accumulator

Here is a complete `sc_main` example demonstrates how truncation and saturation affect signal processing values.

```cpp
#define SC_INCLUDE_FX // Required to include fixed-point headers
#include <systemc>
#include <iostream>
#include <iomanip>

int sc_main(int argc, char* argv[]) {
    // Suppress default SystemC info messages
    sc_core::sc_report_handler::set_actions("/IEEE_Std_1666/deprecated", sc_core::SC_DO_NOTHING);

    std::cout << "--- SystemC Fixed-Point Tutorial ---" << std::endl;

    // 1. Basic Declaration
    // wl = 8, iwl = 4 -> 4 integer bits, 4 fractional bits.
    // Signed type, so range is [-8.0, 7.9375]
    sc_dt::sc_fixed<8, 4> basic_val;
    basic_val = 3.5;
    std::cout << "Basic Value: " << basic_val << std::endl;

    // 2. Exploring Quantization (Rounding vs Truncation)
    // Source number needs high precision
    sc_dt::sc_fixed<16, 4> high_prec = 2.6875; // 0010.1011

    // Target: only 2 fractional bits.
    // Truncation (Default)
    sc_dt::sc_fixed<6, 4, sc_dt::SC_TRN> trn_val = high_prec;
    // Rounding (Adds to LSB)
    sc_dt::sc_fixed<6, 4, sc_dt::SC_RND> rnd_val = high_prec;

    std::cout << "\n--- Quantization ---" << std::endl;
    std::cout << "Original  (16,4): " << high_prec << std::endl;
    std::cout << "Truncated (6,4) : " << trn_val << " (Lost precision)" << std::endl;
    std::cout << "Rounded   (6,4) : " << rnd_val << " (Rounded up)" << std::endl;

    // 3. Exploring Overflow (Wrap vs Saturation)
    // Source number needs high integer range
    sc_dt::sc_fixed<8, 8> large_val = 14;

    // Target: only 3 integer bits (signed, range [-4, 3])
    // Wrap (Default)
    sc_dt::sc_fixed<5, 3, sc_dt::SC_TRN, sc_dt::SC_WRAP> wrap_val = large_val;
    // Saturation
    sc_dt::sc_fixed<5, 3, sc_dt::SC_TRN, sc_dt::SC_SAT> sat_val = large_val;

    std::cout << "\n--- Overflow ---" << std::endl;
    std::cout << "Original   (8,8): " << large_val << std::endl;
    std::cout << "Wrapped    (5,3): " << wrap_val << " (Rolled over)" << std::endl;
    std::cout << "Saturated  (5,3): " << sat_val  << " (Clipped to max positive)" << std::endl;

    // 4. Bit-level Introspection
    std::cout << "\n--- Bit-level introspection ---" << std::endl;
    // sc_fixed allows reading/writing individual bits using []
    // Bits are indexed from 0 (LSB) to wl-1 (MSB)
    sc_dt::sc_fixed<4, 4> mask = 5; // Binary 0101
    std::cout << "Value: " << mask << ", Binary: ";
    for (int i = 3; i >= 0; --i) {
        std::cout << mask[i];
    }
    std::cout << std::endl;

    // Flip the MSB (Sign bit)
    mask[3] = 1;
    std::cout << "After flipping MSB: " << mask << std::endl;

    return 0;
}
```

## LRM Strictness: `#define SC_INCLUDE_FX`

By default, the SystemC header (`#include <systemc>`) **does not** include the fixed-point library. The fixed-point headers bring a massive amount of template code into your translation unit, drastically slowing down compilation.

The LRM specifies that you **must** define the macro `SC_INCLUDE_FX` *before* including `<systemc>` in any file that uses fixed-point types.

```cpp
#define SC_INCLUDE_FX
#include <systemc>
```

If you forget this, you will receive "type not declared" errors from the compiler for `sc_fixed`.

## Conclusion

Understanding `sc_fixed` and `sc_q_mode`/`sc_o_mode` is critical for designing DSP algorithms, Neural Network accelerators, and modem pipelines in SystemC. By utilizing bit-accurate datatypes and favoring `sc_fixed_fast` where applicable, you achieve a perfect balance between hardware accuracy and simulation speed.

## Lesson 101: Deep Dive: async_request_update and External Thread Integration

Canonical lesson: https://www.learn-systemc.com/tutorials/097-deep-dive-async-request-update-and-external-thread

Examine the SystemC scheduler mechanics for handling OS-level threads, interrupts, and asynchronous updates using async_request_update.

## How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

# Deep Dive: `async_request_update` and External Thread Integration

One of the most complex challenges in Virtual Platform (VP) modeling is interfacing the deterministic, cooperative SystemC simulation kernel with the chaotic, preemptive world of the host Operating System (OS).

Imagine a SystemC model that needs to react to external physical stimuli:
- A POSIX `std::thread` listening on a TCP/IP socket for a debugger connection.
- An external UI framework (like Qt) sending user button presses to a mocked GPIO pin.
- A real hardware interrupt arriving via a PCIe driver wrapper.

You **cannot** simply call `sc_event::notify()` or `request_update()` from these external OS threads. The IEEE 1666 SystemC kernel is explicitly **not thread-safe**. Calling kernel APIs from an external OS thread will cause data races, corrupt the scheduler's event queues, and result in segmentation faults.

To solve this, the LRM provides `sc_prim_channel::async_request_update()`. This tutorial dives into how the Accellera source code implements this via Host OS semaphores and how you can safely build external bridges.

## Standard and source context

## The Kernel Reality: `async_request_update()`

In the standard primitive channel pattern, `request_update()` appends the channel pointer directly to `sc_simcontext::m_update_list`. This list is not protected by mutexes.

Introduced in SystemC 2.3, `async_request_update()` provides a thread-safe mechanism. If you look at `src/sysc/kernel/sc_simcontext.cpp`, the kernel maintains a separate queue called `m_async_update_list` and a host OS synchronization primitive (typically an `sc_host_mutex` and an `sc_host_semaphore`).

When an external thread calls `async_request_update()`:
1. The kernel acquires the `sc_host_mutex`.
2. It pushes the `sc_prim_channel*` into `m_async_update_list`.
3. It releases the mutex.
4. It signals the `sc_host_semaphore`. If the SystemC kernel thread was sleeping (blocked in `sc_pause()` or waiting for the next timed event), this semaphore wakes the OS thread running the SystemC scheduler.

Inside the SystemC thread, during the transition into the next evaluation phase, `sc_simcontext::crunch()` calls a method to check `pending_async_updates()`. If true, the kernel safely locks the mutex, moves elements from `m_async_update_list` into the standard `m_update_list`, and then calls the `update()` method of your channelâ€”**this happens safely inside the SystemC kernel thread, free from race conditions.**

## Designing an Asynchronous Channel Bridge

To cross the boundary from OS thread to SystemC thread, you must design a custom primitive channel inheriting from `sc_prim_channel`.

### Step 1: The LRM Rules
1. Your channel must inherit from `sc_prim_channel`.
2. The external OS thread must interact with the channel via custom thread-safe queues or atomic variables.
3. Once the OS thread alters the shared data, it calls `async_request_update()`.
4. The channel overrides the virtual `update()` method. Inside `update()`, you are back in the SystemC thread, so it is safe to pop the shared data and call `sc_event::notify()`.

### Step 2: The End-to-End Implementation

The following 100% LRM-compliant code demonstrates a custom channel that listens for "network packets" coming from an external `std::thread`.

```cpp
#include <systemc>
#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <chrono>

// -------------------------------------------------------------------------
// 1. The Custom Primitive Channel
// -------------------------------------------------------------------------
class AsyncNetworkBridge : public sc_core::sc_prim_channel {
private:
    std::mutex m_mutex;
    std::queue<int> m_packet_queue;
    sc_core::sc_event m_packet_event;

public:
    explicit AsyncNetworkBridge(sc_core::sc_module_name name)
        : sc_core::sc_prim_channel(name) {}

    // ---------------------------------------------------------
    // API for the External OS Thread (e.g., std::thread)
    // ---------------------------------------------------------
    void inject_packet_from_os_thread(int packet_id) {
        {
            // Protect our custom queue with a standard C++ mutex
            std::lock_guard<std::mutex> lock(m_mutex);
            m_packet_queue.push(packet_id);
        }

        // CRITICAL: Notify the SystemC kernel asynchronously.
        // The Accellera kernel locks its sc_host_mutex and adds 'this'
        // to m_async_update_list.
        this->async_request_update();
    }

    // ---------------------------------------------------------
    // SystemC Kernel Update Phase Callback
    // ---------------------------------------------------------
    // The kernel moves 'this' to m_update_list and calls update()
    // safely from the primary SystemC thread context.
    void update() override {
        bool has_data = false;
        {
            std::lock_guard<std::mutex> lock(m_mutex);
            has_data = !m_packet_queue.empty();
        }

        if (has_data) {
            // It is now perfectly safe to notify SystemC events!
            m_packet_event.notify(sc_core::SC_ZERO_TIME);
        }
    }

    // ---------------------------------------------------------
    // API for SystemC Modules
    // ---------------------------------------------------------
    const sc_core::sc_event& default_event() const {
        return m_packet_event;
    }

    bool read_packet(int& out_packet) {
        std::lock_guard<std::mutex> lock(m_mutex);
        if (m_packet_queue.empty()) return false;

        out_packet = m_packet_queue.front();
        m_packet_queue.pop();
        return true;
    }
};

// -------------------------------------------------------------------------
// 2. The SystemC Consumer Module
// -------------------------------------------------------------------------
class Processor : public sc_core::sc_module {
public:
    AsyncNetworkBridge* bridge;

    SC_HAS_PROCESS(Processor);

    Processor(sc_core::sc_module_name name) : sc_core::sc_module(name) {
        SC_THREAD(process_packets);
    }

    void process_packets() {
        while (true) {
            // Wait for the bridge to notify us of a new packet
            wait(bridge->default_event());

            int packet;
            while (bridge->read_packet(packet)) {
                std::cout << "@" << sc_core::sc_time_stamp()
                          << " Processor handled packet ID: " << packet
                          << std::endl;
            }
        }
    }
};

// -------------------------------------------------------------------------
// 3. sc_main and Thread Lifecycle Management
// -------------------------------------------------------------------------
int sc_main(int argc, char* argv[]) {
    sc_core::sc_report_handler::set_actions("/IEEE_Std_1666/deprecated", sc_core::SC_DO_NOTHING);

    // Instantiate our custom async bridge
    AsyncNetworkBridge network_bridge("network_bridge");

    // Instantiate consumer
    Processor cpu("cpu");
    cpu.bridge = &network_bridge;

    // Launch the external OS thread
    // This simulates an external system (like a TCP listener)
    std::thread external_listener([&]() {
        for (int i = 1; i <= 3; ++i) {
            std::this_thread::sleep_for(std::chrono::milliseconds(50));
            std::cout << "[OS Thread] Injecting packet " << i << std::endl;
            network_bridge.inject_packet_from_os_thread(i);
        }
    });

    std::cout << "Starting SystemC simulation..." << std::endl;

    // Run simulation. We use sc_start() with a specific time because
    // waiting on async events indefinitely can cause the kernel to starve
    // if there are no native SystemC events pending.
    sc_core::sc_start(200, sc_core::SC_MS);

    std::cout << "Simulation finished." << std::endl;

    // Clean up OS thread
    if (external_listener.joinable()) {
        external_listener.join();
    }

    return 0;
}
```

## Potential Pitfalls: Kernel Starvation and `sc_pause`

A crucial detail of `sc_simcontext::crunch()` is how it handles idle time. If the simulation reaches a point where `m_timed_events` is empty and `m_runnable` is empty, the `sc_start()` loop will terminate, assuming the simulation is finished.

If your SystemC model is purely reactive and waiting exclusively on `async_request_update()` from an OS thread, the kernel will see an empty event queue and exit instantly, terminating your process!

To prevent this, you must either:
1. Ensure a periodic dummy event (like a clock `SC_THREAD` that calls `wait(1, SC_MS)`) keeps the kernel alive.
2. Use `sc_start()` with an explicit maximum time, running the simulation in time slices.
3. Use the `sc_pause()` architecture and `sc_start()` re-entry if you run the kernel inside another application loop.

## Conclusion

The `async_request_update()` method is your singular bridge between standard C++ multi-threading and the deterministic SystemC execution engine. By strictly guarding shared state with C++ mutexes and deferring event notification to the overridden `update()` phase, you can safely integrate network interfaces, hardware-in-the-loop (HIL) systems, and interactive GUIs into any SystemC virtual platform.

## Lesson 102: Source Deep Dive: sc_time and Timed Event Queues

Canonical lesson: https://www.learn-systemc.com/tutorials/098-source-deep-dive-sc-time-and-timed-event-queues

How SystemC represents time, schedules timed notifications, and advances simulation time without losing precision.

## How to Read This Lesson

If delta cycles explain "what happens before time moves," `sc_time` explains what it means for time to move at all. Read this lesson as the bridge between the LRM scheduling algorithm and the C++ data structures that hold future work.

## Standard and source context

Use `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for time resolution, `sc_time`, timed notification, and scheduler semantics. In source, inspect `Accellera SystemC GitHub repository`, `sc_event.*`, and `sc_simcontext.*`.

## Why SystemC Time Is Not Just a double

Hardware simulation cannot afford floating-point drift. If one process waits for `0.1 ns` ten times and another waits for `1 ns` once, the simulator must make a deterministic decision about whether those events meet at the same time.

So SystemC represents simulation time using an integer value scaled by a global time resolution. User code can construct time values with units like `SC_NS` or `SC_PS`, but internally the simulator compares integer ticks.

The practical consequence: choose time resolution before simulation starts, and do not expect arbitrary real-number precision after the model is elaborated.

## Minimal Example: Timed Notification

```cpp
#include <systemc>
using namespace sc_core;

SC_MODULE(TimerDemo) {
  sc_event later;

  SC_CTOR(TimerDemo) {
    SC_THREAD(producer);
    SC_THREAD(consumer);
  }

  void producer() {
    later.notify(sc_time(10, SC_NS));
  }

  void consumer() {
    wait(later);
    std::cout << "woke at " << sc_time_stamp() << "\n";
  }
};

int sc_main(int, char*[]) {
  TimerDemo top{"top"};
  sc_start();
  return 0;
}
```

The producer does not sleep. It inserts a future notification into the kernel's timed-event structure. The consumer sleeps on the event. When no runnable process remains, the scheduler advances time to the earliest timed event and wakes the sensitive process.

## How the Source Makes This Work

The implementation has three responsibilities:

1. `sc_time` stores normalized simulation time.
2. `sc_event` owns notification state and knows whether it has immediate, delta, or timed notification pending.
3. `sc_simcontext` advances to the next timed notification only when the runnable queue and update work are empty.

That separation matters. A timed notification cannot interrupt the current evaluation phase. The kernel first completes evaluation/update/delta work at the current timestamp. Only then can physical simulation time advance.

## Immediate, Delta, and Timed Notification

| Notification | When it can wake a process | Typical use |
| --- | --- | --- |
| `notify()` | current evaluation phase | rare; use carefully because it can create immediate loops |
| `notify(SC_ZERO_TIME)` | next delta cycle | model combinational propagation without advancing time |
| `notify(delay)` | future simulation time | timers, clocks, loosely timed delays |

If a model depends on exact order between two processes woken at the same timestamp, it is probably relying on non-portable behavior. The LRM defines the scheduling phases, but process order inside a phase is not a design contract.

## Review Checklist

- Is time resolution set before time objects are created?
- Are timed notifications used only when real simulation time should advance?
- Would `SC_ZERO_TIME` express the intent better than a tiny nonzero delay?
- Does the model avoid relying on ordering between processes woken at the same time?
- Are local-time tricks in TLM clearly separated from global `sc_time_stamp()`?

## Lesson 103: Source Deep Dive: sc_event_queue

Canonical lesson: https://www.learn-systemc.com/tutorials/099-source-deep-dive-sc-event-queue

How sc_event_queue models repeated timed notifications and why it is useful for timers, delayed work, and modeling adapters.

## How to Read This Lesson

`sc_event` is a single named event. `sc_event_queue` is what you reach for when a channel or helper object needs to manage many future notifications through one observable event.

## Standard and source context

Use `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for event and timed notification semantics. In source, inspect `Accellera SystemC GitHub repository` and the event scheduling machinery in `sc_event.*` and `sc_simcontext.*`.

## The Problem sc_event_queue Solves

Suppose a peripheral schedules several future interrupts. You could allocate a separate `sc_event` per interrupt, but that becomes awkward when events are dynamic.

`sc_event_queue` lets you do this:

```cpp
SC_MODULE(Device) {
  sc_core::sc_event_queue irq_queue;

  SC_CTOR(Device) {
    SC_METHOD(on_irq);
    sensitive << irq_queue.default_event();
    dont_initialize();
  }

  void schedule_irq(sc_core::sc_time delay) {
    irq_queue.notify(delay);
  }

  void on_irq() {
    std::cout << "IRQ event at " << sc_core::sc_time_stamp() << "\n";
  }
};
```

The user-facing mental model is simple: every `notify(delay)` schedules work, and the queue exposes a default event that fires when the next queued notification matures.

## How the Source Makes This Work

Internally, an event queue manages timed notifications and exposes a single event-like interface to the rest of the model. It is a helper around the same scheduler idea you already know: future work is stored until simulation time reaches the scheduled point.

The important distinction is that `sc_event` generally has one pending timed notification state, while an event queue is designed for multiple queued notifications.

## Where This Shows Up in Real Models

Use an event queue for:

- delayed peripheral interrupts
- modeling timer compare events
- retry scheduling after bus contention
- adapters that convert external timestamps into SystemC events
- rate-limited callbacks where several future activations may already be known

Avoid it when a normal `sc_event` or `wait(delay)` is enough. Extra scheduling machinery makes the model harder to reason about if you do not need it.

## Review Checklist

- Can more than one future notification be pending?
- Is the default event used for sensitivity instead of exposing internals?
- Are events canceled or allowed to drain deliberately?
- Does the model document whether multiple notifications at the same timestamp collapse or produce repeated activations?
- Would a normal `sc_event` be clearer?

## Lesson 104: Source Deep Dive: sc_spawn and Dynamic Processes

Canonical lesson: https://www.learn-systemc.com/tutorials/100-source-deep-dive-sc-spawn-and-dynamic-processes

How dynamic processes are created, configured, named, made sensitive, and reviewed safely.

## How to Read This Lesson

Most SystemC designs should create their structure during elaboration. Dynamic processes are the exception you use when the model really needs runtime-created behavior. This lesson teaches the power and the caution sign together.

## Standard and source context

Use `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for process creation and simulation phase rules. In source, inspect `Accellera SystemC GitHub repository`, `sc_spawn_options.*`, `sc_process.*`, `sc_method_process.*`, and `sc_thread_process.*`.

## Static Process First

Prefer this when possible:

```cpp
SC_CTOR(Top) {
  SC_THREAD(worker);
}
```

The kernel sees the process during elaboration. The hierarchy is stable. Debugging is easier.

Dynamic spawning is for cases where process count or behavior is not naturally known until runtime:

- modeling dynamically created software tasks
- testbench workers created by a scenario
- protocol monitors attached after configuration
- temporary timeout watchers

## Minimal Dynamic Spawn

```cpp
#include <systemc>
using namespace sc_core;

SC_MODULE(Spawner) {
  SC_CTOR(Spawner) {
    SC_THREAD(run);
  }

  void run() {
    sc_spawn_options opt;
    opt.spawn_method();
    opt.dont_initialize();

    sc_event kick;
    opt.set_sensitivity(&kick);

    sc_spawn([&] {
      std::cout << "dynamic method at " << sc_time_stamp() << "\n";
    }, "dynamic_method", &opt);

    kick.notify(SC_ZERO_TIME);
    wait(SC_ZERO_TIME);
  }
};
```

This is not a recommended production pattern because the lambda captures a local event by reference. It is shown to make the mechanics visible. In production, make sure captured objects outlive the spawned process.

## How the Source Makes This Work

`sc_spawn_options` describes what kind of process should be created:

- method or thread
- initialization behavior
- static sensitivity
- stack size or process control options where supported

`sc_spawn` then creates the corresponding process object and registers it with the current simulation context. After that, the dynamic process participates in the same runnable queues, waits, resets, and events as static processes.

That is the key idea: dynamic process creation changes when a process enters the kernel, not the fundamental scheduler semantics.

## Lifetime Trap

Dynamic processes often capture C++ objects. The kernel can keep the process alive longer than the stack frame that created it.

Risky:

```cpp
void launch() {
  int local = 42;
  sc_spawn([&] { std::cout << local << "\n"; });
}
```

Safer:

```cpp
auto value = std::make_shared<int>(42);
sc_spawn([value] { std::cout << *value << "\n"; });
```

For models that need strict lifetime control, prefer owning spawned state in an `sc_module` member or a dedicated object with clear shutdown rules.

## Review Checklist

- Why can this not be a static `SC_THREAD` or `SC_METHOD`?
- Does every captured object outlive the spawned process?
- Is the process name stable and useful in reports?
- Is sensitivity configured explicitly?
- Is cleanup or termination behavior documented?
- Does the design avoid creating unbounded numbers of processes?

## Lesson 105: Source Deep Dive: sc_clock and Reset Implementation

Canonical lesson: https://www.learn-systemc.com/tutorials/101-source-deep-dive-sc-clock-and-reset-implementation

How clocks schedule edges, how reset policies attach to processes, and why reset behaves like controlled process unwinding.

## How to Read This Lesson

Clocks and resets look simple in examples, but they are where simulation semantics meet hardware intuition. Read this lesson slowly: the source explains why reset is not just an `if` statement around your thread body.

## Standard and source context

Use `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for clocks, events, processes, and reset semantics. Use `Docs/LRMs/SystemC_Synthesis_Subset_1_4_7.pdf` when the reset must synthesize. In source, inspect `Accellera SystemC GitHub repository`, `Accellera SystemC GitHub repository`, `sc_process.*`, and `sc_except.*`.

## How sc_clock Behaves

`sc_clock` is a predefined channel that produces periodic value changes. It is useful because it combines:

- a Boolean signal-like value
- positive and negative edge events
- period, duty cycle, start time, and edge scheduling
- integration with the kernel's timed event queue

When a process is sensitive to `clk.pos()`, it is really sensitive to an event finder associated with the clock's positive edge event.

```cpp
SC_METHOD(tick);
sensitive << clk.pos();
dont_initialize();
```

The process runs when the clock channel updates and the positive edge event is notified.

## Reset Is Attached to a Process

Reset declarations such as:

```cpp
SC_CTHREAD(run, clk.pos());
reset_signal_is(rst, true);
```

do not simply store a Boolean in your module. They attach reset metadata to the process object created most recently by the process macro.

That is why ordering matters: the reset call configures the process registration, not an arbitrary C++ function.

## How the Source Makes Reset Work

The Accellera implementation tracks reset targets through kernel-side reset structures. When reset becomes active, the process is marked for reset. For thread-like processes, the kernel may need to unwind the coroutine stack and restart the process function from its reset point.

That unwinding is represented through a special exception mechanism. It is not an error exception in the normal application sense. It is controlled kernel flow used to stop the current coroutine execution and return to a known process state.

## Why This Matters for User Code

This is why destructors and local objects inside a thread can matter during reset. If reset unwinds the stack, normal C++ cleanup rules may run. If your thread owns nontrivial local state across waits, reset behavior must be reviewed.

Better pattern:

```cpp
void run() {
  state = IDLE;
  out.write(false);
  wait();

  while (true) {
    // normal clocked behavior
    wait();
  }
}
```

The reset section is explicit and repeatable.

## Review Checklist

- Is reset attached immediately after the intended process registration?
- Does reset behavior match the process type?
- Are local objects inside resettable threads safe to unwind?
- Is synthesis reset style checked separately from simulation reset behavior?
- Are clock periods and time resolution chosen deliberately?

## Lesson 106: Core API and Source Symbol Field Guide

Canonical lesson: https://www.learn-systemc.com/tutorials/131-core-api-and-source-symbol-field-guide

Individual SystemC core symbols from the LRM and Accellera source, explained as a source-reading map for experienced developers.

## How to Read This Lesson

This is a field guide for the names that show up when you move from normal tutorials into the LRM and the Accellera source tree. Do not try to memorize every private helper. Use each symbol as a breadcrumb: public modeling concept first, C++ implementation mechanism second.

## Standard and source context

Use `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for the normative API and behavior. Use `.codex-src/systemc/src/sysc` for implementation reading, especially `kernel`, `communication`, `datatypes`, `tracing`, and `utils`.

## Object Model and Attributes

`sc_object` is the root of the visible hierarchy story. Most things a user can name, find, or inspect inherit from it or cooperate with it. `sc_attr_base`, `sc_attribute`, and `sc_attr_cltn` are the lightweight attribute mechanism around that object model. They are not the same as CCI parameters. Think of them as attached metadata for SystemC objects, while CCI is a configuration and inspection standard with brokers, originators, callbacks, and mutability rules.

`sc_find_object` and `sc_find_event` are lookup helpers. They are convenient in debug tools, but production model connectivity should usually be explicit through constructors, ports, exports, and configuration objects.

`sc_hierarchy_scope`, `sc_object_host`, `sc_module_name`, and `sc_gen_unique_name` belong to the naming and construction machinery. When a module constructor runs, the implementation needs to know which parent is currently being elaborated. That is why names can become hierarchical without you manually passing every parent path.

## Process and Simulation Context

`sc_simcontext` is the simulation kernel owner. It tracks elaboration, process registries, event queues, time, runnable processes, update requests, and simulation status. When you call `sc_start`, you are not just entering a loop; you are asking this context to run the scheduled world that elaboration built.

`sc_curr_proc_kind` tells implementation code what kind of process is active: method, thread, cthread, or no process. This matters for APIs such as `wait`, dynamic sensitivity, and originator-like behavior where the library needs to know who is calling.

`sc_elab_and_sim`, `sc_end_of_simulation_invoked`, `sc_delta_count_at_current_time`, and related status helpers are guardrails. They let the implementation reject operations at the wrong time and let tooling ask whether the kernel is elaborating, simulating, ending, or already stopped.

`sc_get_stop_mode`, `sc_pending_activity`, `sc_pending_activity_at_current_time`, `sc_pending_activity_at_future_time`, `sc_get_time_resolution`, `sc_max_time`, `sc_get_top_level_events`, and `sc_objects` are introspection and simulation-state helpers. They are most useful in tools, diagnostics, and tests that need to ask the kernel what remains scheduled.

`sc_interrupt_here`, `sc_is_unwinding`, `sc_join`, `sc_cthread_handle`, `sc_cor_std_thread`, `sc_cor_pthread`, and `sc_cor_pkg_std_thread` live near process control and coroutine/thread implementation. Model authors normally do not use these names directly, but they explain why `wait` can suspend a SystemC thread without returning to your own C++ loop.

The stop/suspend vocabulary includes `sc_set_stop_mode`, `sc_stop_mode`, `sc_stop_here`, `sc_status`, `sc_starvation_policy`, `sc_time_to_pending_activity`, `sc_suspend_all`, `sc_unsuspend_all`, `sc_suspendable`, and `sc_unsuspendable`. Read these as kernel-control and scheduler-state concepts. They are useful in simulation control and diagnostics, but most models should still express behavior through events, waits, and clear termination policy.

## Events and Sensitivity Helpers

`sc_event_finder`, `sc_event_finder_t`, `sc_event_and_expr`, and `sc_event_or_expr` are the C++ machinery behind nice sensitivity syntax. When you write `sensitive << clk.pos()`, the implementation cannot store a magic expression; it stores objects that can later locate the event associated with the interface.

`sc_sensitive`, `sc_sensitive_pos`, and `sc_sensitive_neg` are builder-style helpers for static sensitivity. They exist because the `SC_METHOD` and `SC_THREAD` registration APIs need a readable way to attach events after the process object exists.

`sc_event_queue` is different from a plain event. It queues notifications for future times, which makes it useful for delayed callbacks and timeout-like behavior. If your model only needs immediate or delta notification, a plain `sc_event` is simpler.

## Ports, Exports, and Binding Internals

`sc_port`, `sc_in`, `sc_inout`, `sc_out`, `sc_export`, `sc_export_base`, and `sc_bind_proxy` form the binding surface. The user-level story is simple: ports call interfaces, exports publish interfaces, and channels implement interfaces. The source-level story is that elaboration builds and checks those links before simulation starts.

`sc_port_registry`, `sc_export_registry`, `sc_module_registry`, `sc_reset_finder`, `sc_port_policy`, `sc_hierarchical_name_exists`, `sc_register_hierarchical_name`, `sc_unregister_hierarchical_name`, `sc_context_begin`, `sc_context`, `sc_name_gen`, and `sc_descendant_inclusion_info` are part of the registration and hierarchy infrastructure. When you see them in the source, read them as bookkeeping for "what has been built, where does it live, and which rules apply to this connection?"

`sc_signal_write_if`, `sc_signal_channel`, `sc_semaphore_if`, `sc_unbound`, `sc_unnamed`, `sc_stub`, `sc_tie`, `sc_switch`, `sc_bind_ef`, `sc_bind_elem`, and `sc_event_expr` describe communication edges, fallback binding states, event expressions, and interface contracts. They are the kind of names you inspect when a binding or sensitivity problem happens before time zero.

`sc_fifo_blocking_in_if`, `sc_fifo_blocking_out_if`, and `sc_fifo_out_if` are examples of narrow interface contracts. They let a channel expose exactly the operations a client should see, instead of exposing a concrete FIFO object.

`sc_in_resolved`, `sc_in_rv`, `sc_inout_resolved`, `sc_inout_rv`, `sc_out_resolved`, and `sc_out_rv` are resolved-signal port variants. Reach for them only when the model really has multi-driver semantics. For most platform models, a single-writer signal or an explicit channel is easier to review.

## Bit and Logic Vector Proxies

The datatype implementation uses proxy objects heavily. Names such as `sc_proxy`, `sc_bitref`, `sc_bitref_r`, `sc_concref`, `sc_concref_r`, `sc_concatref`, `sc_value_base`, `sc_int_bitref`, `sc_int_bitref_r`, `sc_uint_bitref_r`, `sc_signed_bitref_r`, `sc_signed_subref`, `sc_signed_subref_r`, `sc_unsigned_subref`, and `sc_unsigned_subref_r` are how the library makes expressions like bit selects, ranges, and concatenations feel natural in C++.

Additional source names such as `sc_int_base`, `sc_int_subref`, `sc_int_subref_r`, `sc_uint_subref_r`, `sc_unsigned_proxy`, `sc_signed_proxy`, `sc_unsigned_bitref`, `sc_cref`, `sc_generic_base`, `sc_abs`, `sc_min`, `sc_max`, `sc_enc`, and `sc_behavior` show up around numeric expression support and generic helper layers. Treat them as datatype implementation vocabulary unless the LRM exposes the behavior directly.

The practical lesson is this: bit-accurate datatypes are powerful, but expressions may create temporary proxy objects. In hot platform paths, prefer plain C++ integers unless bit width, four-state logic, signedness, or synthesis intent really matters.

## Fixed-Point and Numeric Policy Symbols

`sc_fixed`, `sc_ufixed`, `sc_fix`, `sc_ufix`, `sc_fix_fast`, `sc_ufix_fast`, `sc_fxnum`, `sc_fxnum_fast_observer`, `sc_fxnum_bitref`, `sc_fxnum_fast_bitref`, `sc_fxcast_switch`, `sc_fmt`, `sc_numrep`, `sc_length_param`, and `sc_big_op_info` sit in the fixed-point and numeric policy area.

`sc_fxcast_context`, `sc_fxtype_context`, `sc_length_context`, `sc_fxnum_bitref_r`, `sc_fxnum_fast_bitref_r`, `sc_fxnum_subref_r`, `sc_fxnum_fast_subref`, `sc_fxnum_fast_subref_r`, `sc_fxval_observer`, and `sc_fxval_fast_observer` are context/proxy/observer names behind fixed-point expression evaluation. They are useful when diagnosing why a fixed-point expression quantized, rounded, or overflowed differently than expected.

The LRM gives the portable behavior. The source shows the cost: quantization, overflow, casting, and range operations are library work, not free machine instructions. Use them when the numeric contract is part of the hardware model. Avoid them when a fast architectural model only needs approximate integer or floating-point behavior.

## Reports and Assertions

`sc_assertion_failed`, `sc_report`, and the report-handler family are the difference between a model that fails clearly and one that fails as a C++ mystery. Prefer `SC_REPORT_ERROR`, `SC_REPORT_WARNING`, and `SC_REPORT_INFO` over scattered `std::cout` once the message is part of model behavior.

Implementation names such as `sc_msg_def`, `sc_stage_callback_registry`, `sc_stage_callback_if`, `sc_register_stage_callback`, `sc_unregister_stage_callback`, `sc_phash_base`, `sc_phash_elem`, `sc_phash_base_iter`, `sc_plist_base`, `sc_plist_base_iter`, `sc_plist_elem`, `sc_pvector`, `sc_vpool`, `sc_mempool`, and `sc_cmnhdr` are utility infrastructure. They are not modeling APIs, but they show how the reference implementation organizes messages, callbacks, pools, lists, and hash tables.

`sc_argc`, `sc_argv`, and `sc_exception` are the bridge back to ordinary C++ process startup and exception flow. They matter for integration code and test harnesses more than for day-to-day model structure.

Other low-level source names that help you navigate the implementation are `sc_entry_func`, `sc_global`, `sc_carry`, `sc_utils_ids`, `sc_kernel_ids`, `sc_communication_ids`, `sc_int_ids`, `sc_nbdefs`, `sc_time_tuple`, `sc_digits`, `sc_proxy_traits`, `sc_simcontext_int`, `sc_curr_proc_handle`, `sc_cor_fn`, `sc_cor_fiber`, `sc_cor_pkg_fiber`, `sc_cor_pkg_pthread`, `sc_cor_pkg_qt`, `sc_scoped_lock`, `sc_ref`, `sc_vector_assembly`, `sc_vector_init_policy`, and `sc_write_comment`. They are implementation and support vocabulary, not the public modeling surface.

## Review Pattern

When reviewing unfamiliar SystemC source, ask four questions:

- Is this symbol public API, LRM-defined behavior, or implementation support?
- Does it participate in elaboration, simulation scheduling, communication, datatype expression building, tracing, or reporting?
- Does the model rely on a portable contract or an implementation detail?
- Would a simpler C++ type or explicit channel make the behavior easier to maintain?

## Lesson 107: VP Architecture & Design

Canonical lesson: https://www.learn-systemc.com/tutorials/102-vp-architecture-and-design

Designing the memory map and overall architecture for a multi-component Virtual Platform using standard TLM patterns.

## How to Read This Lesson

# Building a Virtual Platform: Architecture

Up until this chapter, we have looked at isolated SystemC concepts: ports, events, TLM sockets, and memory management. But how do you combine all of these into a massive, bootable **Virtual Platform (VP)**?

In this multi-part tutorial, we will build a completely functional Virtual Platform from scratch.

We will strictly adhere to the industry-standard **Doulos Simple Bus** patterns and Accellera TLM 2.0 AT/LT open-source paradigms. This ensures our architecture is vendor-neutral, highly interoperable, and LRM compliant.

## Standard and source context

## The Goal

We are going to build a System-on-Chip (SoC) comprising:
1. **A CPU Wrapper (Initiator):** A mock Instruction Set Simulator (ISS) that initiates TLM Loosely Timed (LT) memory-mapped read/write transactions.
2. **A Router (Interconnect):** A TLM bus that routes the CPU's generic payload transactions to the correct peripheral based on the memory address.
3. **A RAM Module (Target):** A contiguous block of memory.
4. **A Timer Peripheral (Target):** A memory-mapped hardware timer.

## The Memory Map

Every memory-mapped SoC relies on a Memory Map. When the CPU writes to physical address `0x4000_0004`, the Router must determine which target socket to forward the transaction to.

Here is the hardware memory map for our Virtual Platform:

| Peripheral | Base Address | Size | End Address |
| :--- | :--- | :--- | :--- |
| **RAM** | `0x0000_0000` | 256 KB (`0x40000`) | `0x0003_FFFF` |
| **Timer** | `0x4000_0000` | 4 KB (`0x1000`) | `0x4000_0FFF` |
| **UART** | `0x4001_0000` | 4 KB (`0x1000`) | `0x4001_0FFF` |

## Virtual Platform Skeleton Example & Kernel Mechanics

The following is a complete, runnable skeleton of the VP top-level architecture. It demonstrates how to instantiate the initiator, the router, and the targets, and bind them using `simple_initiator_socket` and `simple_target_socket`.

**Under the Hood (Accellera TLM Kernel):**
Why do we use `tlm_utils::simple_initiator_socket` instead of `tlm::tlm_initiator_socket`?
The raw base sockets require you to explicitly implement both the forward transport interface (`tlm_fw_transport_if`) and backward path (`tlm_bw_transport_if`), inheriting from them manually on your `sc_module`. The `tlm_utils` sockets automatically encapsulate this boilerplate, providing `register_b_transport()` callback macros to cleanly route C++ member functions.

Furthermore, how does a transaction travel from the CPU to the Router?
TLM 2.0 is built on `sc_core::sc_port`. During the elaboration phase, `cpu.socket.bind(router.target_socket)` resolves the port proxies. When `run_program()` calls `socket->b_transport()`, it does **not** involve an OS context switch or an event queue. Because the port has been statically resolved to a pointer during elaboration, the CPU's thread executes `router.b_transport` as a direct, blocking C++ function call. The thread context (the stack of `run_program`) literally extends down into the Router and RAM. This is why TLM LT simulation is so incredibly fast.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>

// 1. Mock CPU Initiator
SC_MODULE(CPU_Initiator) {
    tlm_utils::simple_initiator_socket<CPU_Initiator> socket;

    SC_CTOR(CPU_Initiator) : socket("cpu_socket") {
        SC_THREAD(run_program);
    }

    void run_program() {
        tlm::tlm_generic_payload trans;
        sc_core::sc_time delay = sc_core::SC_ZERO_TIME;

        // Mock writing to the RAM (Address 0x0000_0004)
        uint32_t data = 0xDEADBEEF;
        trans.set_command(tlm::TLM_WRITE_COMMAND);
        trans.set_address(0x00000004);
        trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
        trans.set_data_length(4);
        trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

        std::cout << "@" << sc_core::sc_time_stamp() << " [CPU] Sending Write to 0x4" << std::endl;
        socket->b_transport(trans, delay);

        wait(delay); // Advance time based on interconnect/target delay
    }
};

// 2. Mock Peripheral Target (RAM)
SC_MODULE(RAM_Target) {
    tlm_utils::simple_target_socket<RAM_Target> socket;

    SC_CTOR(RAM_Target) : socket("ram_socket") {
        socket.register_b_transport(this, &RAM_Target::b_transport);
    }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        std::cout << "@" << sc_core::sc_time_stamp() << " [RAM] Received transaction at offset 0x"
                  << std::hex << trans.get_address() << std::endl;

        trans.set_response_status(tlm::TLM_OK_RESPONSE);
        delay += sc_core::sc_time(10, sc_core::SC_NS); // Add RAM latency
    }
};

// 3. Simple Router (Interconnect)
SC_MODULE(SimpleRouter) {
    tlm_utils::simple_target_socket<SimpleRouter> target_socket;
    // Multi-port socket for multiple peripherals (Doulos Simple Bus pattern)
    tlm_utils::simple_initiator_socket<SimpleRouter> init_socket_ram;

    SC_CTOR(SimpleRouter) : target_socket("target_socket"), init_socket_ram("init_socket_ram") {
        target_socket.register_b_transport(this, &SimpleRouter::b_transport);
    }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        uint64_t addr = trans.get_address();

        // Memory Map Decoder Logic
        if (addr >= 0x00000000 && addr <= 0x0003FFFF) {
            // Forward to RAM
            init_socket_ram->b_transport(trans, delay);
        } else {
            trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
        }
        delay += sc_core::sc_time(2, sc_core::SC_NS); // Add Routing Latency
    }
};

// 4. Top-Level Virtual Platform
SC_MODULE(VirtualPlatform) {
    CPU_Initiator cpu;
    SimpleRouter  router;
    RAM_Target    ram;

    SC_CTOR(VirtualPlatform) : cpu("cpu"), router("router"), ram("ram") {
        // Bind Initiator -> Router -> Targets
        cpu.socket.bind(router.target_socket);
        router.init_socket_ram.bind(ram.socket);
    }
};

int sc_main(int argc, char* argv[]) {
    VirtualPlatform vp("vp");
    sc_core::sc_start(100, sc_core::SC_NS);
    return 0;
}
```

## Component Breakdown

1. **The Initiator (CPU):** Creates the `tlm_generic_payload`, sets the command, physical address, and data pointer, and calls `b_transport()`. It then yields via `wait(delay)` to synchronize the Loosely Timed quantum.
2. **The Interconnect (Router):** Implements a decode function based on the memory map. Before forwarding the transaction to a target via an initiator socket array, a production router will subtract the base address (e.g., `addr - 0x4000_0000`) so the peripheral only observes a relative local offset.
3. **The Targets (Peripherals):** Execute the read/write logic on their internal memory arrays, advance the delay reference by their inherent processing latency, and set `TLM_OK_RESPONSE`.

## Source-reading checkpoint

For configurable platforms, inspect `.codex-src/cci` beside the top-level model. The broker and parameter path should explain which platform choices are fixed and which remain tool-controlled.

## Lesson 108: The VP Router (Interconnect)

Canonical lesson: https://www.learn-systemc.com/tutorials/103-the-vp-router-interconnect

Building a TLM-2.0 interconnect to decode addresses and route transactions to peripherals using Doulos Simple Bus concepts.

## How to Read This Lesson

# Building a Virtual Platform: The Router

The Router (or Interconnect) is the central address decoder of any Virtual Platform. It sits between the initiators (CPUs, DMAs) and the targets (Memory, Peripherals).

When an initiator sends a read transaction for physical address `0x4000_0004`, the Router must determine which target socket is mapped to that address, subtract the target's base address, forward the transaction, and properly restore the address on the return path. Now let's look at how the Accellera TLM kernel enforces this.

## Standard and source context

## The Complete Router Example

This example demonstrates a strictly LRM-compliant, Doulos Simple Bus-style TLM 2.0 Router. It connects to a mock CPU and routes to two generic memory blocks acting as RAM and Timer.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <vector>

// 1. The Interconnect Router
SC_MODULE(VPRouter) {
    // Single target socket facing the CPU
    tlm_utils::simple_target_socket<VPRouter> target_socket;

    // Array of initiator sockets facing the peripherals
    std::vector<tlm_utils::simple_initiator_socket<VPRouter>*> init_sockets;

    SC_CTOR(VPRouter) : target_socket("target_socket") {
        target_socket.register_b_transport(this, &VPRouter::b_transport);

        // Dynamically create two sockets for our 2 peripherals (RAM and Timer)
        init_sockets.push_back(new tlm_utils::simple_initiator_socket<VPRouter>("init_ram"));
        init_sockets.push_back(new tlm_utils::simple_initiator_socket<VPRouter>("init_timer"));
    }

    ~VPRouter() {
        for (auto* socket : init_sockets) delete socket;
    }

private:
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        sc_dt::uint64 original_address = trans.get_address();
        sc_dt::uint64 local_address = 0;
        int target_port = -1;

        // Address Decoding Map
        if (original_address >= 0x00000000 && original_address <= 0x0003FFFF) {
            // RAM Region (256 KB)
            target_port = 0;
            local_address = original_address - 0x00000000;
        }
        else if (original_address >= 0x40000000 && original_address <= 0x40000FFF) {
            // Timer Region (4 KB)
            target_port = 1;
            local_address = original_address - 0x40000000; // Subtract Base Address!
        }

        if (target_port != -1) {
            // Modify the transaction address so the peripheral sees a local offset (0x0 to 0xFFF)
            trans.set_address(local_address);

            std::cout << "@" << sc_core::sc_time_stamp()
                      << " [Router] Routing global addr 0x" << std::hex << original_address
                      << " to port " << target_port << " as local addr 0x" << local_address << std::endl;

            // Forward transaction
            (*init_sockets[target_port])->b_transport(trans, delay);

            // Restoring Address (CRITICAL RULE)
            trans.set_address(original_address);
        } else {
            std::cout << "@" << sc_core::sc_time_stamp()
                      << " [Router] ERROR: Address 0x" << std::hex << original_address
                      << " is unmapped." << std::endl;
            trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
        }

        // Add minimal routing delay
        delay += sc_core::sc_time(2, sc_core::SC_NS);
    }
};

// 2. Mock CPU Initiator
SC_MODULE(MockCPU) {
    tlm_utils::simple_initiator_socket<MockCPU> socket;
    SC_CTOR(MockCPU) : socket("socket") { SC_THREAD(run); }

    void run() {
        tlm::tlm_generic_payload trans;
        sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
        uint32_t data = 0;

        trans.set_command(tlm::TLM_READ_COMMAND);
        trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
        trans.set_data_length(4);

        // Send a transaction to the Timer base address
        trans.set_address(0x40000000);
        socket->b_transport(trans, delay);

        // Send a transaction to an unmapped address
        trans.set_address(0xFFFFFFFF);
        socket->b_transport(trans, delay);
    }
};

// 3. Mock Peripheral
SC_MODULE(MockPeripheral) {
    tlm_utils::simple_target_socket<MockPeripheral> socket;
    SC_CTOR(MockPeripheral) : socket("socket") {
        socket.register_b_transport(this, &MockPeripheral::b_transport);
    }
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
    }
};

// 4. Top Level
int sc_main(int argc, char* argv[]) {
    MockCPU cpu("cpu");
    VPRouter router("router");
    MockPeripheral ram("ram");
    MockPeripheral timer("timer");

    // Bindings
    cpu.socket.bind(router.target_socket);
    (*router.init_sockets[0]).bind(ram.socket);
    (*router.init_sockets[1]).bind(timer.socket);

    sc_core::sc_start();
    return 0;
}
```

## The Address Restoration Rule

A target peripheral should never care that it is mapped at `0x4000_0000`. It only cares about its internal offsets (e.g., register `0x00` is Control, `0x04` is Counter). This allows the IP block to be perfectly reusable across any SoC project. The Router achieves this by passing `original_address - base_address`.

However, the CPU initiator expects the payload it sent to return unaltered.
**Under the Hood:** In the `tlm_generic_payload` implementation, `set_address(uint64_t)` literally overwrites the internal `m_address` integer field. Therefore, after the target returns from `b_transport`, the router **must** immediately restore the original address. If it fails to do so, advanced initiators that inspect returning payloads to match transaction queues will crash or corrupt simulation memory.

## Error Handling

If an address doesn't match any mapped region, the Router immediately assigns `tlm::TLM_ADDRESS_ERROR_RESPONSE` and returns. **Under the Hood:** This is simply an assignment to the `m_response_status` enum field in the payload. The initiator CPU will inspect this response and typically trigger a hardware exception (e.g., an ARM Data Abort) inside its Instruction Set Simulator (ISS).

## DMI Routing (Direct Memory Interface)

While not shown in this simple example, production routers also implement `get_direct_mem_ptr`.
**Under the Hood:** If the CPU requests DMI, the router passes the request down to the target memory. The target returns a `tlm_dmi` struct containing a raw `unsigned char*` pointer and its local bounds (e.g., `0x0` to `0x3FFFF`). Before the router returns this struct to the CPU, it MUST add the base address back into the DMI bounds (e.g., `0x00000000` to `0x0003FFFF`). This allows the CPU's Instruction Set Simulator to directly pointer-dereference addresses in the global map without invoking the socket hierarchy, speeding up simulation by orders of magnitude.

## Source-reading checkpoint

For router implementation work, inspect `tlm_target_socket`, `tlm_initiator_socket`, and `b_transport` call paths. The source trail helps separate address decoding policy from socket plumbing.

## Lesson 109: The VP Peripherals

Canonical lesson: https://www.learn-systemc.com/tutorials/104-the-vp-peripherals

Building RAM and Hardware Timer memory-mapped peripherals using TLM 2.0 Simple Target Sockets.

## How to Read This Lesson

# Building a Virtual Platform: The Peripherals

In the previous step, our Router forwarded transactions to specific sockets based on physical memory addresses. Now, we build the targets on the other side of those sockets: a **Memory block (RAM)** and a **Hardware Timer**.

Now let's look at how the Accellera TLM 2.0 core standardizes these patterns.

## Standard and source context

## Complete Peripheral Example

Here is a complete, runnable example demonstrates the exact TLM 2.0 implementations for a standard RAM array and a register-based Hardware Timer.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>

// 1. A Simple TLM RAM Peripheral
class RAM_Peripheral : public sc_core::sc_module {
public:
    tlm_utils::simple_target_socket<RAM_Peripheral> socket;

    SC_HAS_PROCESS(RAM_Peripheral);
    RAM_Peripheral(sc_core::sc_module_name name, unsigned int size_bytes)
        : sc_core::sc_module(name), size(size_bytes) {

        memory = new unsigned char[size];
        memset(memory, 0, size);
        socket.register_b_transport(this, &RAM_Peripheral::b_transport);
    }
    ~RAM_Peripheral() { delete[] memory; }

private:
    unsigned char* memory;
    unsigned int size;

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        tlm::tlm_command cmd = trans.get_command();
        sc_dt::uint64    adr = trans.get_address();
        unsigned char*   ptr = trans.get_data_ptr();
        unsigned int     len = trans.get_data_length();

        // Check if the transaction exceeds the boundaries of this specific RAM
        if (adr + len > size) {
            trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
            return;
        }

        if (cmd == tlm::TLM_READ_COMMAND) {
            memcpy(ptr, &memory[adr], len);
        } else if (cmd == tlm::TLM_WRITE_COMMAND) {
            memcpy(&memory[adr], ptr, len);
        }

        // Advance simulation time to model access latency
        delay += sc_core::sc_time(10, sc_core::SC_NS);
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
    }
};

// 2. A Register-Based Hardware Timer Peripheral
class Timer_Peripheral : public sc_core::sc_module {
public:
    tlm_utils::simple_target_socket<Timer_Peripheral> socket;

    SC_HAS_PROCESS(Timer_Peripheral);
    Timer_Peripheral(sc_core::sc_module_name name) : sc_core::sc_module(name) {
        socket.register_b_transport(this, &Timer_Peripheral::b_transport);
        SC_THREAD(timer_tick_thread);
    }

private:
    bool running = false;
    unsigned int counter = 0;
    sc_core::sc_event start_event;

    void timer_tick_thread() {
        while(true) {
            // Wait for software to enable the timer
            if (!running) wait(start_event);

            // Wait 1 microsecond hardware tick
            wait(1, sc_core::SC_US);
            if (running) counter++;
        }
    }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        sc_dt::uint64 adr = trans.get_address();
        unsigned char* ptr = trans.get_data_ptr();

        if (trans.get_command() == tlm::TLM_WRITE_COMMAND) {
            if (adr == 0x00) {
                // Offset 0x00: Control Register (Write 1 to Start)
                uint32_t val;
                // Strict-aliasing compliant extraction
                memcpy(&val, ptr, sizeof(val));
                running = (val != 0);
                if (running) {
                    std::cout << "@" << sc_core::sc_time_stamp() << " [Timer] Started." << std::endl;
                    // Under the hood: this notify() is executing on the CPU's thread!
                    start_event.notify();
                } else {
                    std::cout << "@" << sc_core::sc_time_stamp() << " [Timer] Stopped." << std::endl;
                }
            }
        } else if (trans.get_command() == tlm::TLM_READ_COMMAND) {
            if (adr == 0x04) {
                // Offset 0x04: Counter Register (Read Only)
                memcpy(ptr, &counter, sizeof(counter));
                std::cout << "@" << sc_core::sc_time_stamp() << " [Timer] CPU Read Counter: " << counter << std::endl;
            }
        }

        delay += sc_core::sc_time(5, sc_core::SC_NS);
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
    }
};

// 3. Mock Initiator to Drive the Timer
SC_MODULE(CPU_Driver) {
    tlm_utils::simple_initiator_socket<CPU_Driver> socket;
    SC_CTOR(CPU_Driver) : socket("socket") { SC_THREAD(run); }

    void run() {
        tlm::tlm_generic_payload trans;
        sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
        uint32_t data = 1; // Start command

        // 1. Write 1 to Timer Control Register (Offset 0x00)
        trans.set_command(tlm::TLM_WRITE_COMMAND);
        trans.set_address(0x00);
        trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
        trans.set_data_length(4);
        socket->b_transport(trans, delay);
        wait(delay);

        // 2. Wait for 5 microseconds of simulation time to pass
        wait(5, sc_core::SC_US);

        // 3. Read from Timer Counter Register (Offset 0x04)
        delay = sc_core::SC_ZERO_TIME;
        trans.set_command(tlm::TLM_READ_COMMAND);
        trans.set_address(0x04);
        socket->b_transport(trans, delay);
        wait(delay);
    }
};

int sc_main(int argc, char* argv[]) {
    CPU_Driver cpu("cpu");
    Timer_Peripheral timer("timer");

    // Direct binding for demonstration
    cpu.socket.bind(timer.socket);

    sc_core::sc_start();
    return 0;
}
```

## Architectural Design and Thread Context

- **The RAM Module** simply allocates memory and uses `memcpy` to move data directly. It serves as a generic bulk storage endpoint. Notice we use `memcpy` exclusively. Attempting to cast the payload's `unsigned char* ptr` to `uint32_t*` violates C++ strict aliasing rules, causing undefined behavior or crashes depending on the underlying CPU architecture running the simulation.
- **The Hardware Timer Module** is modeled around **Memory-Mapped Registers**. Instead of moving bulk memory, its `b_transport` acts as an `if/else` switch statement, triggering specific internal C++ events or threads when a particular offset (e.g., `0x00`) is written to.
- **Thread Context Awareness:** It is critical to understand that `b_transport` is just a virtual function call. When the `Timer_Peripheral` invokes `start_event.notify()`, that invocation is happening *on the CPU's thread context*. The event `notify()` simply injects a wakeup task into the `sc_simcontext` scheduler. The CPU thread returns from `b_transport`, yields using `wait(delay)`, and then the scheduler successfully wakes the `timer_tick_thread` in the next delta cycle.

## Source-reading checkpoint

For a peripheral model, follow its `tlm_target_socket` callback into `b_transport`, then trace register access. This keeps transport mechanics separate from device behavior. If the peripheral is configurable, continue into `.codex-src/cci` before treating a reset value as fixed policy.

## Lesson 110: The CPU Wrapper (ISS)

Canonical lesson: https://www.learn-systemc.com/tutorials/105-the-cpu-wrapper-iss

Wrapping a simple instruction sequence to act as a bus master, and assembling the final Virtual Platform.

## How to Read This Lesson

# Building a Virtual Platform: The CPU Wrapper

In a production Virtual Platform (VP), the CPU is usually an Instruction Set Simulator (ISS) provided by a vendor like ARM (FastModels) or Imperas. The ISS decodes cross-compiled `.elf` binaries. Whenever the executing software performs a Load (`LDR`) or Store (`STR`) to physical memory, the ISS generates a TLM transaction and pushes it out its initiator socket.

For our VP, we will build a "Dummy CPU" acting as the bus master. It executes a hardcoded sequence of TLM reads and writes to configure the Timer and test the RAM. Now let's look at how this operates within the Accellera kernel.

## Standard and source context

## Memory Management in Production CPUs

**Under the Hood:** In our simple example below, we allocate a `tlm::tlm_generic_payload trans` directly on the stack for convenience. However, a production ISS executing an operating system might execute 100 million memory instructions per second. If the CPU wrapper were to call `new tlm_generic_payload()` for every transaction, the C++ heap allocator overhead would destroy simulation performance.
The IEEE 1666 standard solves this by providing the `tlm_mm_interface`. Production CPU wrappers instantiate a global Memory Manager that maintains a pre-allocated array of payload objects. The ISS calls `trans = mm->allocate()`, populates it, and sends it. Once the transaction completes, it calls `trans->release()`, returning the object to the pool without any `delete` or heap fragmentation.

## The Complete Assembled Virtual Platform

This file acts as the culmination of the Virtual Platform chapter. It contains the Mock CPU, the Router (from Chapter 12.2), the RAM, the Timer (from 12.3), and the `sc_main` that binds them together.

This model perfectly conforms to the Accellera Simple Bus AT/LT specifications.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <vector>

// --- Targets (Peripherals) ---
class RAM : public sc_core::sc_module {
public:
    tlm_utils::simple_target_socket<RAM> socket;
    unsigned char* memory;

    RAM(sc_core::sc_module_name name) : sc_core::sc_module(name) {
        memory = new unsigned char[0x40000]; // 256 KB
        socket.register_b_transport(this, &RAM::b_transport);
    }
    ~RAM() { delete[] memory; }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        if (trans.get_command() == tlm::TLM_WRITE_COMMAND) {
            memcpy(&memory[trans.get_address()], trans.get_data_ptr(), trans.get_data_length());
        }
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
        delay += sc_core::sc_time(10, sc_core::SC_NS);
    }
};

class Timer : public sc_core::sc_module {
public:
    tlm_utils::simple_target_socket<Timer> socket;
    unsigned int counter = 0;

    Timer(sc_core::sc_module_name name) : sc_core::sc_module(name) {
        socket.register_b_transport(this, &Timer::b_transport);
    }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        if (trans.get_command() == tlm::TLM_WRITE_COMMAND && trans.get_address() == 0x0) {
            counter = 100; // Mock setting the timer
        }
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
    }
};

// --- Router (Interconnect) ---
class Router : public sc_core::sc_module {
public:
    tlm_utils::simple_target_socket<Router> target_socket;
    tlm_utils::simple_initiator_socket<Router> init_ram;
    tlm_utils::simple_initiator_socket<Router> init_timer;

    Router(sc_core::sc_module_name name) : sc_core::sc_module(name), target_socket("target_socket"), init_ram("init_ram"), init_timer("init_timer") {
        target_socket.register_b_transport(this, &Router::b_transport);
    }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        sc_dt::uint64 addr = trans.get_address();

        if (addr >= 0x00000000 && addr <= 0x0003FFFF) {
            init_ram->b_transport(trans, delay);
        } else if (addr >= 0x40000000 && addr <= 0x40000FFF) {
            trans.set_address(addr - 0x40000000);
            init_timer->b_transport(trans, delay);
            trans.set_address(addr); // Restore!
        } else {
            trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
        }
    }
};

// --- Initiator (Mock CPU) ---
class CPU : public sc_core::sc_module {
public:
    tlm_utils::simple_initiator_socket<CPU> socket;

    SC_HAS_PROCESS(CPU);
    CPU(sc_core::sc_module_name name) : sc_core::sc_module(name), socket("socket") {
        SC_THREAD(execute_firmware);
    }

private:
    void execute_firmware() {
        tlm::tlm_generic_payload trans;
        sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
        unsigned int data;

        std::cout << "@" << sc_core::sc_time_stamp() << " [CPU] Booting..." << std::endl;

        // 1. Write to Timer Control (Address 0x40000000)
        data = 1;
        trans.set_command(tlm::TLM_WRITE_COMMAND);
        trans.set_address(0x40000000);
        trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
        trans.set_data_length(4);
        trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

        socket->b_transport(trans, delay);

        // Under the Hood: Context Switch
        // The b_transport call completed. We yield to the sc_simcontext
        // scheduler to advance global time by the accumulated 'delay'.
        wait(delay);

        // 2. Write to RAM (Address 0x00000100)
        delay = sc_core::SC_ZERO_TIME;
        data = 0xDEADBEEF;
        trans.set_address(0x00000100);
        trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

        socket->b_transport(trans, delay);
        wait(delay);

        std::cout << "@" << sc_core::sc_time_stamp() << " [CPU] Firmware execution halted." << std::endl;
    }
};

// --- Top Level Assembly ---
int sc_main(int argc, char* argv[]) {
    CPU cpu("cpu");
    Router router("router");
    RAM ram("ram");
    Timer timer("timer");

    // Bind CPU -> Router
    cpu.socket.bind(router.target_socket);

    // Bind Router -> Peripherals
    router.init_ram.bind(ram.socket);
    router.init_timer.bind(timer.socket);

    std::cout << "Starting Virtual Platform Simulation..." << std::endl;
    sc_core::sc_start();

    return 0;
}
```

## Conclusion

Congratulations! You have built a fully functional Electronic System Level (ESL) Virtual Platform.

You constructed standard targets (RAM, Timer), a Doulos-compliant routing interconnect, and an initiator that acts as the bus master. Silicon companies use variations of this exact architecture to develop, test, and boot massive operating systems (like Linux or Android) months or years before the physical silicon is ever manufactured in a fab.

## Source-reading checkpoint

For an ISS wrapper, follow `b_transport` from the initiator side and keep `tlm_quantumkeeper` nearby. The wrapper is where instruction execution and model time must stay honest.

## Lesson 111: VP Requirements & Abstraction Level

Canonical lesson: https://www.learn-systemc.com/tutorials/106-vp-requirements-and-abstraction-level

Defining Loosely Timed (LT) vs Approximately Timed (AT) models and gathering requirements for a Virtual Platform.

## How to Read This Lesson

# VP Requirements & Abstraction Level

When architecting a Virtual Platform (VP), the first question an Electronic System Level (ESL) architect must answer is: *What is the abstraction level?*

If the goal is to boot an operating system (Linux, Android) as fast as possible to develop software early, you need **Loosely Timed (LT)** models. If the goal is to analyze bus contention, memory bandwidth, and cache hit ratios, you need **Approximately Timed (AT)** models. Now let's look at how the Accellera TLM standard defines these mechanically.

## Standard and source context

## Loosely Timed (LT) vs Approximately Timed (AT)

The IEEE 1666 TLM 2.0 standard explicitly defines these two coding styles.

### Loosely Timed (LT)
- **Goal:** Maximum simulation speed.
- **Mechanism:** Uses the `b_transport` blocking interface. **Under the Hood:** A transaction executes in a single C++ function call down the port-proxy stack, bypassing the `sc_simcontext` event queue entirely. Time is passed as a reference (`sc_core::sc_time& delay`) and accumulated, utilizing Temporal Decoupling to avoid expensive scheduler context switches.
- **Use Case:** Software development, firmware validation, functional verification.

### Approximately Timed (AT)
- **Goal:** Cycle-approximate performance analysis.
- **Mechanism:** Uses the `nb_transport_fw` and `nb_transport_bw` non-blocking interfaces. **Under the Hood:** A single transaction is broken into multiple phases using the `tlm_phase` enum (`BEGIN_REQ`, `END_REQ`, `BEGIN_RESP`, `END_RESP`). Components usually utilize a Payload Event Queue (`tlm_utils::peq_with_cb_and_phase`) which heavily relies on `sc_event::notify()` to schedule payload processing at exact future delta cycles or timestamps. This accurately models pipeline stages and bus arbitration but leads to thousands of `sc_simcontext` wakeups per transaction.
- **Use Case:** Architectural exploration, performance bottleneck analysis.

## End-to-End LT Initiator Example with Accellera Quantum Keeper

In a Doulos Simple Bus compliant VP, we predominantly use LT to boot software. Here is a perfect LT initiator utilizing the standard `tlm_utils::tlm_quantumkeeper` to manage Temporal Decoupling safely.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/tlm_quantumkeeper.h>

SC_MODULE(LT_CPU_Model) {
    tlm_utils::simple_initiator_socket<LT_CPU_Model> socket;
    tlm_utils::tlm_quantumkeeper m_qk;

    SC_CTOR(LT_CPU_Model) : socket("socket") {
        // Set the global quantum (e.g., sync every 1000 ns)
        m_qk.set_global_quantum(sc_core::sc_time(1000, sc_core::SC_NS));
        // Reset the local time offset
        m_qk.reset();

        SC_THREAD(execute_instructions);
    }

    void execute_instructions() {
        tlm::tlm_generic_payload trans;
        uint32_t data = 0;

        // Temporal Decoupling: Accumulate time locally without yielding to the SystemC scheduler
        for (int i = 0; i < 1000; i++) {
            trans.set_command(tlm::TLM_READ_COMMAND);
            trans.set_address(0x1000);
            trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
            trans.set_data_length(4);
            trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

            // Fetch the current local delay from the quantum keeper
            sc_core::sc_time local_delay = m_qk.get_local_time();

            socket->b_transport(trans, local_delay);

            // Add internal CPU instruction execution latency
            local_delay += sc_core::sc_time(10, sc_core::SC_NS);

            // Update the quantum keeper with the new accumulated local delay
            m_qk.set(local_delay);

            // The quantum keeper automatically checks if (local_delay >= global_quantum).
            // If it is, it calls wait() under the hood, syncing with sc_simcontext,
            // and resets local_time to SC_ZERO_TIME.
            if (m_qk.need_sync()) {
                std::cout << "@" << sc_core::sc_time_stamp() << " [CPU] Quantum exceeded. Syncing scheduler." << std::endl;
                m_qk.sync();
            }
        }
    }
};

int sc_main(int argc, char* argv[]) {
    // Boilerplate for standalone compilation
    return 0;
}
```

By explicitly gathering requirements upfront and utilizing the native `tlm_quantumkeeper`, you avoid the disastrous mistake of writing slow, cycle-accurate models when the software team just needs a fast functional platform.

## Lesson 112: VP Memory Map and Register Contract

Canonical lesson: https://www.learn-systemc.com/tutorials/107-vp-memory-map-and-register-contract

Defining the memory map and software-to-hardware register contracts for Virtual Platform peripherals.

## How to Read This Lesson

# VP Memory Map and Register Contract

In a Virtual Platform, hardware behavior is entirely dictated by **Memory-Mapped Registers**. The CPU (running embedded C firmware) configures physical IP blocks by writing specific bit patterns to specific hexadecimal offsets.

This creates a rigid contract between the software and the hardware. If the C code expects the UART Transmit Register to be at offset `0x04`, the SystemC peripheral *must* intercept transactions at `0x04` and trigger transmission logic.

## Standard and source context

## The Register Contract

A well-architected peripheral does not hardcode global addresses like `0x4000_1004`. It relies on the Router to subtract the base address, so it only observes local offsets (e.g., `0x00` to `0xFF`).

### Example: UART Register Contract

| Offset | Register Name | Access | Description |
| :--- | :--- | :--- | :--- |
| `0x00` | `UART_CTRL` | R/W | Bit 0: Enable. Bit 1: TX Interrupt Enable. |
| `0x04` | `UART_STATUS` | R/O | Bit 0: TX Ready. Bit 1: RX Full. |
| `0x08` | `UART_TX_DATA` | W/O | Write 8-bit character to transmit. |

## End-to-End Register Peripheral Example

Here is a complete `sc_main` example models the UART contract defined above, strictly utilizing TLM 2.0 payload mechanics.

**Under the Hood (TLM Kernel Data Arrays):**
In `tlm_generic_payload`, the payload data is managed via a raw `unsigned char* m_data`. The standard assumes Host Endianness. When extracting a 32-bit integer from this byte array, we use `memcpy`. This is not just a stylistic choice; modern C++ compilers enforce strict-aliasing rules. Casting `unsigned char*` directly to `uint32_t*` can cause undefined behavior or alignment faults (SIGBUS) on architectures like ARM. `memcpy` is explicitly optimized by compilers and guarantees safe TLM data extraction.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>

SC_MODULE(UART_Peripheral) {
    tlm_utils::simple_target_socket<UART_Peripheral> socket;

    // Internal Register State
    uint32_t reg_ctrl = 0;
    uint32_t reg_status = 0x01; // Initial state: TX Ready is true (Bit 0 = 1)

    SC_CTOR(UART_Peripheral) : socket("socket") {
        socket.register_b_transport(this, &UART_Peripheral::b_transport);
    }

private:
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        sc_dt::uint64 addr = trans.get_address();
        unsigned char* ptr = trans.get_data_ptr();
        unsigned int len = trans.get_data_length();

        if (len != 4) { // Enforce 32-bit aligned access
            trans.set_response_status(tlm::TLM_BURST_ERROR_RESPONSE);
            return;
        }

        if (trans.get_command() == tlm::TLM_WRITE_COMMAND) {
            handle_write(addr, ptr);
        } else if (trans.get_command() == tlm::TLM_READ_COMMAND) {
            handle_read(addr, ptr);
        }

        delay += sc_core::sc_time(10, sc_core::SC_NS);
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
    }

    void handle_write(sc_dt::uint64 addr, unsigned char* ptr) {
        uint32_t val;
        // Strict aliasing compliant extraction
        memcpy(&val, ptr, 4);

        switch (addr) {
            case 0x00: // UART_CTRL
                reg_ctrl = val;
                std::cout << "@" << sc_core::sc_time_stamp() << " [UART] Control Reg updated: 0x"
                          << std::hex << reg_ctrl << std::endl;
                break;
            case 0x08: // UART_TX_DATA
                if (reg_ctrl & 0x01) { // If UART is enabled
                    std::cout << "@" << sc_core::sc_time_stamp() << " [UART] Transmitted char: "
                              << (char)(val & 0xFF) << std::endl;
                }
                break;
            default:
                SC_REPORT_WARNING("UART", "Write to read-only or invalid register.");
                break;
        }
    }

    void handle_read(sc_dt::uint64 addr, unsigned char* ptr) {
        uint32_t val = 0;
        switch (addr) {
            case 0x00: val = reg_ctrl; break;
            case 0x04: val = reg_status; break;
            default:   val = 0; break;
        }
        memcpy(ptr, &val, 4);
    }
};

int sc_main(int argc, char* argv[]) {
    UART_Peripheral uart("uart");
    // Standalone compilation check
    return 0;
}
```

By strictly honoring the register contract mapped by the embedded software engineers and adhering to the raw byte-pointer logic demanded by the TLM 2.0 generic payload, the Virtual Platform acts as a perfect hardware digital twin. This enables native, unmodified Linux drivers to boot directly on the SystemC models exactly as they would on the physical silicon.

## Source-reading checkpoint

For memory-map code, inspect `tlm_target_socket` dispatch beside register helpers. The implementation trail should make each decoded address range easy to audit.

## Lesson 113: Router DMI and Debug Transport

Canonical lesson: https://www.learn-systemc.com/tutorials/108-router-dmi-and-debug-transport

Optimizing VP performance with Direct Memory Interface (DMI) and backdoor Debug Transport.

## How to Read This Lesson

# Router DMI and Debug Transport

A Loosely Timed (LT) Virtual Platform booting an operating system executes millions of memory instructions (Instruction Fetches, Stack Pushes). If every single memory access has to allocate a `tlm_generic_payload`, traverse the router, decode the address, and execute a `b_transport` function call, the simulation will be devastatingly slow.

The IEEE 1666 standard provides **Direct Memory Interface (DMI)** to bypass the socket transport entirely. Let's dig into the TLM kernel source to see how this works mechanically.

## Standard and source context

## Direct Memory Interface (DMI)

DMI allows an initiator to request a direct C++ pointer to the target's physical memory array.
1. CPU attempts an access.
2. CPU asks the Router for DMI permissions for that address region.
3. Router forwards the request to RAM.
4. RAM returns a `tlm_dmi` struct containing the raw `unsigned char*` pointer and the allowed address range.
5. CPU caches this pointer and executes millions of future reads/writes using direct array indexing (`ptr[offset]`), achieving native execution speed.

## Complete DMI Target Example

This `sc_main` example implements a DMI-compliant RAM module.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>

class DMI_RAM : public sc_core::sc_module {
public:
    tlm_utils::simple_target_socket<DMI_RAM> socket;
    unsigned char* memory;
    unsigned int size;

    SC_HAS_PROCESS(DMI_RAM);
    DMI_RAM(sc_core::sc_module_name name, unsigned int size_bytes)
        : sc_core::sc_module(name), size(size_bytes) {

        memory = new unsigned char[size];
        memset(memory, 0, size);

        // Standard Transport
        socket.register_b_transport(this, &DMI_RAM::b_transport);
        // Register DMI Hook
        socket.register_get_direct_mem_ptr(this, &DMI_RAM::get_direct_mem_ptr);
    }

    ~DMI_RAM() { delete[] memory; }

private:
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        // Standard transport logic omitted...

        // Hint to the initiator that DMI is available for this region
        trans.set_dmi_allowed(true);
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
    }

    // LRM DMI Method
    bool get_direct_mem_ptr(tlm::tlm_generic_payload& trans, tlm::tlm_dmi& dmi_data) {
        // Provide the direct C++ pointer to the memory array
        dmi_data.set_dmi_ptr(memory);

        // Define the valid physical range for this pointer (Local offsets)
        dmi_data.set_start_address(0x00000000);
        dmi_data.set_end_address(size - 1);

        // Allow both read and write
        dmi_data.set_granted_access(tlm::tlm_dmi::DMI_ACCESS_READ_WRITE);

        // Set latencies so the initiator can accurately accumulate time
        dmi_data.set_read_latency(sc_core::sc_time(10, sc_core::SC_NS));
        dmi_data.set_write_latency(sc_core::sc_time(10, sc_core::SC_NS));

        std::cout << "@" << sc_core::sc_time_stamp() << " [RAM] Granted DMI Access." << std::endl;
        return true;
    }
};

int sc_main(int argc, char* argv[]) {
    DMI_RAM ram("ram", 0x10000); // 64KB RAM
    // Initiator omitted for brevity
    return 0;
}
```

## The Router DMI Address Adjustment Rule

**Under the Hood:** In the `get_direct_mem_ptr` implementation above, the RAM sets the bounds to `0x0` through `0xFFFF` because it only knows its local size. However, the CPU requested a global address (e.g., `0x4000_0000`). If the CPU caches a `tlm_dmi` struct with bounds `0x0-0xFFFF`, its global cache lookup will immediately fail on the next instruction.
Therefore, the **Router must intercept** the returning `tlm_dmi` struct from the target. The router executes `dmi_data.set_start_address( dmi_data.get_start_address() + base_address )` and `dmi_data.set_end_address( dmi_data.get_end_address() + base_address )` before returning it to the CPU. The CPU then calculates the native memory array index as `dmi_ptr[ global_address - dmi_start_address ]`.

## Debug Transport (`transport_dbg`)

When a debugger (like GDB attached to the Virtual Platform) inspects memory, it must NOT alter the hardware state. Reading a UART FIFO should not pop the FIFO.

The LRM provides `transport_dbg`, a completely side-effect-free, non-blocking path. It does not take an `sc_time` argument, and it executes instantaneously in zero simulation time.
**Under the Hood:** Unlike `b_transport`, `transport_dbg` does not rely on `set_response_status`. Instead, the virtual method returns an `unsigned int` representing the exact number of bytes successfully read or written. If the address is invalid, the target simply returns `0`. Targets must implement `transport_dbg` to support architectural inspection utilities and GDB stubs.

## Source-reading checkpoint

For fast paths, inspect `get_direct_mem_ptr`, DMI invalidation, and `transport_dbg` beside the normal socket path. These APIs deliberately bypass different amounts of timing work. Keep `tlm_target_socket` in the trace so each fast path can be compared with normal transport.

## Lesson 114: Peripheral Modeling: GPIO

Canonical lesson: https://www.learn-systemc.com/tutorials/109-peripheral-modeling-gpio

Modeling General Purpose Input/Output (GPIO) pins combining TLM registers and discrete SystemC signals.

## How to Read This Lesson

# Peripheral Modeling: GPIO

So far, our Virtual Platform peripherals (RAM, Timer) have communicated entirely through TLM sockets. However, many peripherals interact with the outside world via physical wires. A General Purpose Input/Output (GPIO) peripheral is the perfect example of a bridge between memory-mapped TLM configuration and standard `sc_signal` hardware pins.

## Standard and source context

## The GPIO Architecture

A standard GPIO peripheral exposes:
1. **TLM Target Socket:** To receive configuration (Pin Direction) and data (Output Value) from the CPU via memory-mapped registers.
2. **Standard SystemC Ports (`sc_out` / `sc_in`):** The actual external hardware pins that toggle high or low.

## Complete GPIO Peripheral Example & TLM Timing Pitfalls

Here is a complete `sc_main` model demonstrates an 8-bit GPIO controller. The CPU sets the pin directions (Input or Output) via the `DIR` register, and drives the pins via the `OUT` register.

**Under the Hood (The LT/DE Sync Problem):**
There is a massive structural pitfall when mixing LT TLM and discrete-event pins. In the code below, `update_hardware_pins()` uses `pins_out.write()`. Because `b_transport` is called by a temporally decoupled LT initiator, the global `sc_time_stamp()` might be `0 ns`, while the `delay` argument is `100 ns`. If you call `pins_out.write()` immediately inside `b_transport`, the standard `sc_signal` calls `request_update()` on the `sc_simcontext`. This schedules the pin to toggle in the immediate Delta cycle at global time `0 ns`, effectively happening *in the past* relative to the CPU's local time. To solve this in production VPs, you cannot call `pins_out.write()` directly inside `b_transport`. You must queue the toggle operation using a Payload Event Queue (PEQ) or schedule a dedicated `SC_METHOD` using `sc_event::notify(delay)` so the discrete pin toggles exactly at `global_time + local_delay`.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>

SC_MODULE(GPIO_Controller) {
    tlm_utils::simple_target_socket<GPIO_Controller> socket;

    // External hardware pins (8-bit bus)
    sc_core::sc_out<sc_dt::sc_bv<8>> pins_out{"pins_out"};

    // Internal Registers
    uint8_t reg_dir = 0x00; // 1 = Output, 0 = Input
    uint8_t reg_out = 0x00; // The logic levels to drive

    SC_CTOR(GPIO_Controller) : socket("socket") {
        socket.register_b_transport(this, &GPIO_Controller::b_transport);

        // Drive initial state
        SC_THREAD(init_pins);
    }

private:
    void init_pins() {
        wait(sc_core::SC_ZERO_TIME);
        update_hardware_pins();
    }

    void update_hardware_pins() {
        // Only drive the bits configured as outputs
        sc_dt::sc_bv<8> current_val = reg_out & reg_dir;

        // WARNING: In a purely Loosely Timed environment, writing here directly
        // causes the toggle to happen at global sc_time_stamp(), ignoring local delay.
        pins_out.write(current_val);

        std::cout << "@" << sc_core::sc_time_stamp()
                  << " [GPIO] Hardware Pins Driven: " << current_val << std::endl;
    }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        sc_dt::uint64 addr = trans.get_address();
        unsigned char* ptr = trans.get_data_ptr();
        unsigned int len = trans.get_data_length();

        if (len != 1) { // Enforce 8-bit access for this simple peripheral
            trans.set_response_status(tlm::TLM_BURST_ERROR_RESPONSE);
            return;
        }

        if (trans.get_command() == tlm::TLM_WRITE_COMMAND) {
            uint8_t val = *ptr;
            if (addr == 0x00) { // DIR Register
                reg_dir = val;
                std::cout << "@" << sc_core::sc_time_stamp() << " [GPIO] DIR Reg = 0x" << std::hex << (int)reg_dir << std::endl;
            } else if (addr == 0x01) { // OUT Register
                reg_out = val;
                std::cout << "@" << sc_core::sc_time_stamp() << " [GPIO] OUT Reg = 0x" << std::hex << (int)reg_out << std::endl;
            }
            // A change in registers triggers a physical pin update
            update_hardware_pins();
        }

        delay += sc_core::sc_time(10, sc_core::SC_NS);
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
    }
};

int sc_main(int argc, char* argv[]) {
    // 1. Hardware Wires
    sc_core::sc_signal<sc_dt::sc_bv<8>> external_bus("external_bus");

    // 2. Instantiate Peripheral
    GPIO_Controller gpio("gpio");
    gpio.pins_out(external_bus); // Bind to external hardware

    // 3. Mock CPU Transaction (Configure and Drive)
    tlm::tlm_generic_payload trans;
    sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
    uint8_t data;

    // Configure lower 4 bits as Outputs
    data = 0x0F;
    trans.set_command(tlm::TLM_WRITE_COMMAND);
    trans.set_address(0x00);
    trans.set_data_ptr(&data);
    trans.set_data_length(1);
    gpio.socket->b_transport(trans, delay);

    // Drive 0xA on the output pins
    data = 0x0A;
    trans.set_address(0x01);
    gpio.socket->b_transport(trans, delay);

    sc_core::sc_start();
    return 0;
}
```

By combining TLM sockets for high-speed software control and standard `sc_signal` objects for physical interactions (with proper delay scheduling), SystemC effectively models the boundary between the processor subsystem and the external printed circuit board.

## Source-reading checkpoint

For GPIO, trace `b_transport` into register writes and then into the modeled signal effect. The source-reading habit is to keep bus access and pin behavior visibly separate. Start that trace at `tlm_target_socket` before entering the device callback. If GPIO reset values are parameterized, continue into `.codex-src/cci` and verify which broker owns the preset.

## Lesson 115: Interrupt Controller & System Events

Canonical lesson: https://www.learn-systemc.com/tutorials/110-interrupt-controller-and-system-events

Modeling an Interrupt Controller (GIC/NVIC) and bridging hardware IRQs into TLM software interrupts.

## How to Read This Lesson

# Interrupt Controllers & System Events

In a real System-on-Chip (SoC), peripherals do not expect the CPU to constantly poll them. When a Timer expires or a UART receives data, it asserts a hardware interrupt line (IRQ).

The **Interrupt Controller** (like the ARM GIC or Cortex-M NVIC) receives dozens of these raw hardware lines, prioritizes them, and signals the CPU. In a Virtual Platform, we must model this exact behavior.

## Standard and source context

## The Architecture

1. **Peripheral:** Asserts a standard `sc_signal<bool>` representing the IRQ line.
2. **Interrupt Controller (INTC):** Contains a TLM socket (for the CPU to read status/acknowledge) and standard `sc_in<bool>` ports for the incoming IRQ lines. It evaluates priority and asserts a single `sc_out<bool>` to the CPU.
3. **CPU ISS:** A thread monitoring the `sc_in<bool>` from the INTC, triggering an asynchronous exception routine in the simulated software.

## Complete Interrupt Controller Example

Here is a complete `sc_main` demonstrates a peripheral generating an interrupt, the controller routing it, and the CPU responding.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>

// 1. Mock Peripheral (Timer that fires an IRQ)
SC_MODULE(Timer_IRQ) {
    sc_core::sc_out<bool> irq_out{"irq_out"};

    SC_CTOR(Timer_IRQ) {
        SC_THREAD(run);
    }
    void run() {
        wait(20, sc_core::SC_NS);
        std::cout << "@" << sc_core::sc_time_stamp() << " [Timer] Firing IRQ." << std::endl;
        irq_out.write(true); // Assert Interrupt
    }
};

// 2. The Interrupt Controller
SC_MODULE(InterruptController) {
    tlm_utils::simple_target_socket<InterruptController> socket;

    sc_core::sc_in<bool>  irq_in{"irq_in"};
    sc_core::sc_out<bool> cpu_irq{"cpu_irq"};

    bool irq_pending = false;

    SC_CTOR(InterruptController) : socket("socket") {
        socket.register_b_transport(this, &InterruptController::b_transport);
        SC_METHOD(eval_interrupts);
        sensitive << irq_in;
    }

private:
    void eval_interrupts() {
        if (irq_in.read() == true) {
            std::cout << "@" << sc_core::sc_time_stamp() << " [INTC] IRQ Received. Forwarding to CPU." << std::endl;
            irq_pending = true;
            cpu_irq.write(true);
        }
    }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        // Mocking the CPU acknowledging and clearing the interrupt
        if (trans.get_command() == tlm::TLM_WRITE_COMMAND && trans.get_address() == 0x10) { // 0x10 = Clear Reg
            std::cout << "@" << sc_core::sc_time_stamp() << " [INTC] CPU Cleared IRQ via TLM." << std::endl;
            irq_pending = false;
            cpu_irq.write(false);
        }
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
    }
};

// 3. Mock CPU
SC_MODULE(MockCPU_IRQ) {
    tlm_utils::simple_initiator_socket<MockCPU_IRQ> socket;
    sc_core::sc_in<bool> irq_in{"irq_in"};

    SC_CTOR(MockCPU_IRQ) : socket("socket") {
        SC_THREAD(cpu_loop);
        // Under the hood: This maps to the sc_signal::m_posedge_event
        sensitive << irq_in.pos();
    }

    void cpu_loop() {
        while(true) {
            wait(); // Wait for IRQ
            std::cout << "@" << sc_core::sc_time_stamp() << " [CPU] INTERRUPT DETECTED! Jumping to ISR." << std::endl;

            // Send TLM transaction to INTC to clear the interrupt
            tlm::tlm_generic_payload trans;
            sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
            uint32_t val = 1;

            trans.set_command(tlm::TLM_WRITE_COMMAND);
            trans.set_address(0x10);
            trans.set_data_ptr(reinterpret_cast<unsigned char*>(&val));
            trans.set_data_length(4);
            trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

            socket->b_transport(trans, delay);
            wait(delay); // Advance time for bus latency
        }
    }
};

int sc_main(int argc, char* argv[]) {
    // Hardware wires
    sc_core::sc_signal<bool> timer_irq_wire;
    sc_core::sc_signal<bool> cpu_irq_wire;

    // Instantiate Modules
    Timer_IRQ timer("timer");
    InterruptController intc("intc");
    MockCPU_IRQ cpu("cpu");

    // Bind Wires
    timer.irq_out(timer_irq_wire);
    intc.irq_in(timer_irq_wire);
    intc.cpu_irq(cpu_irq_wire);
    cpu.irq_in(cpu_irq_wire);

    // Bind TLM Socket
    cpu.socket.bind(intc.socket);

    sc_core::sc_start(100, sc_core::SC_NS);
    return 0;
}
```

## LT Temporal Decoupling vs Hardware Interrupts

There is a severe synchronization issue when mixing TLM Loosely Timed (LT) initiators with discrete hardware events.

**The Problem:** In an LT CPU using a `tlm_quantumkeeper`, the CPU is accumulating `local_time` natively within a `for()` loop, running *ahead* of global `sc_time_stamp()`. If the CPU is currently at local time `40 ns` (but the global scheduler is at `0 ns`), and a timer fires a physical IRQ at global time `20 ns`, the CPU has technically "overshot" the interrupt! The software state has already executed instructions past the point where it should have been preempted by the ISR.

**The Solution:** Production CPU wrappers (like open-source RISC-V ISS models or QEMU-SystemC bridges) must implement a quantum-break mechanism. When `irq_in.pos()` is triggered inside the hardware domain, the CPU wrapper must immediately assert a flag (`m_async_irq_pending`). The LT execution loop must check this flag after every instruction. If asserted, it forces an immediate `wait(local_time)` to sync back with the `sc_simcontext`, processes the ISR, and resets the quantum.

## Lesson 116: Configurable Platforms (CCI)

Canonical lesson: https://www.learn-systemc.com/tutorials/111-configurable-platforms-cci

Integrating IEEE 1666.1 CCI parameters into a complete Virtual Platform environment.

## How to Read This Lesson

# Configurable Platforms (CCI)

A truly reusable Virtual Platform must be highly configurable. A hardware vendor might sell an SoC with 256KB of RAM, while another SKU features 512KB. Rather than maintaining multiple C++ codebases, the Virtual Platform should expose parameters via the IEEE 1666.1 Configuration, Control, and Inspection (CCI) standard.

## Standard and source context

## Integrating CCI in the Virtual Platform

In our completed Virtual Platform, the Router's memory map bounds and the RAM's size should be driven by `cci_param`. A top-level JSON or Python script can inject these parameters into the CCI Broker before elaboration, dynamically constructing the SoC.

## Complete VP Configuration Example

This `sc_main` demonstrates injecting a custom RAM size into the Broker and observing the VP adapting to it during elaboration.

```cpp
#include <systemc>
#include <cci_configuration>

// 1. Configurable Target (RAM)
class ConfigurableRAM : public sc_core::sc_module {
public:
    // CCI Parameter for Memory Size
    cci::cci_param<int> mem_size;
    unsigned char* memory;

    SC_HAS_PROCESS(ConfigurableRAM);

    ConfigurableRAM(sc_core::sc_module_name name)
        : sc_core::sc_module(name),
          // Default size is 1024 bytes if not overridden by the Broker
          mem_size("mem_size", 1024, "Size of the RAM in bytes")
    {
        // Lock the parameter so it cannot change after elaboration
        mem_size.lock();

        // Allocate memory based on the CCI parameter!
        int size = mem_size.get_value();
        memory = new unsigned char[size];

        std::cout << "[ConfigurableRAM] Elaboration: RAM size configured to "
                  << size << " bytes." << std::endl;
    }

    ~ConfigurableRAM() {
        delete[] memory;
    }
};

// 2. VP Top Level
SC_MODULE(VirtualPlatform) {
    ConfigurableRAM ram;

    SC_CTOR(VirtualPlatform) : ram("ram") {}
};

int sc_main(int argc, char* argv[]) {
    // 1. Acquire the Global CCI Broker BEFORE instantiating the VP
    cci::cci_broker_handle broker = cci::cci_get_broker();

    // 2. Set the Preset Configurations
    // In an industrial setting, these values are parsed from a JSON file.
    // The hierarchical name must perfectly match the elaboration tree.
    std::cout << "[System] Injecting custom CCI parameters..." << std::endl;
    broker.set_initial_preset_value("vp_top.ram.mem_size", cci::cci_value(4096)); // Override default 1024

    // 3. Elaborate the Virtual Platform
    // The ConfigurableRAM constructor will now fetch '4096' from the broker.
    VirtualPlatform vp_top("vp_top");

    // 4. Start Simulation
    sc_core::sc_start();

    return 0;
}
```

## The Accellera Kernel Elaboration Flow

**Under the Hood (JSON to Elaboration):**
In industrial Virtual Platform environments, the `sc_main` function begins by parsing a configuration JSON file. The environment translates the JSON objects into `cci::cci_value` generic AST representations and pushes them into the `cci_broker_manager`'s preset map using `set_initial_preset_value()`.

When `VirtualPlatform vp_top("vp_top")` is instantiated, it spawns `ConfigurableRAM ram("ram")`. During the `ram` constructor, the `cci_param` `mem_size` is constructed. The `cci_param` queries the global broker. The broker constructs the full hierarchical name `vp_top.ram.mem_size` by walking up the `sc_object` parent pointers. It then searches its internal unconsumed preset map for this exact string. Finding the `cci_value(4096)` entry, the broker injects it into the parameter, overriding the default `1024`. This happens strictly *before* the `memory = new unsigned char[size]` line executes, dynamically allocating the SoC's memory map directly from the JSON inputs.

By conforming to the CCI standard, your IP blocks become plug-and-play components compatible with enterprise Virtual Platform environments like Synopsys Virtualizer or Cadence Virtual System Platform (VSP).

## Source-reading checkpoint

For configuration wiring, inspect `.codex-src/cci` around `cci_broker_if`, `cci_param_if`, and originators, then follow how the platform consumes presets before traffic starts. For reusable VP patterns, compare the wiring with `.codex-src/systemc-common-practices`.

## Lesson 117: VP Firmware Traffic and Real-World Flow

Canonical lesson: https://www.learn-systemc.com/tutorials/112-vp-firmware-traffic-and-real-world-flow

A complete TLM-2.0 Loosely Timed (LT) VP scenario with firmware traffic, routing, memory access, and timing.

## How to Read This Lesson

# VP Firmware Traffic and Real-World Flow

The final Virtual Platform (VP) should tell a story that accurately represents real firmware bring-up. In an industrial setting, a VP connects standard bus architectures, executes firmware instructions from a CPU initiator, routes transactions through an interconnect, and manipulates target peripherals.

## Standard and source context

## Boot Sequence

Even with a dummy CPU initiator, we can simulate a standard firmware-like sequence:
1. Write a configuration byte to a peripheral.
2. Read a status register from memory.
3. Advance simulation time correctly based on memory latency.

This demonstrates interconnect routing, target response handling, and Loosely Timed (LT) quantum accumulation.

## Standard Doulos / Accellera Interconnect Pattern

To demonstrate this cleanly, we abandon proprietary wrappers and rely exclusively on the IEEE 1666 standard TLM-2.0 `b_transport` interface and a standard Simple Bus / Interconnect model.

In this architecture:
- An **Initiator** generates payload transactions.
- A **Router (Interconnect)** inspects the payload's address and forwards it to the correct target.
- The **Targets** (Memory, Peripherals) implement the `b_transport` interface, apply latency via the `sc_time` reference parameter, and return standard `TLM_OK_RESPONSE` statuses.

## Complete End-to-End Example

The following is a 100% complete, compilable SystemC model of a Loosely Timed VP running a firmware boot traffic scenario over a standard interconnect.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <iostream>

using namespace sc_core;
using namespace tlm;

// ---------------------------------------------------------
// Target 1: Memory (Base Address: 0x0000)
// ---------------------------------------------------------
class MemoryTarget : public sc_module {
public:
    tlm_utils::simple_target_socket<MemoryTarget> socket;
    unsigned char mem[1024];

    SC_HAS_PROCESS(MemoryTarget);
    MemoryTarget(sc_module_name name) : sc_module(name), socket("socket") {
        socket.register_b_transport(this, &MemoryTarget::b_transport);
        for(int i=0; i<1024; ++i) mem[i] = 0; // Initialize memory
        mem[0x10] = 0xAA; // Pre-load a "Boot Status" value
    }

    void b_transport(tlm_generic_payload& trans, sc_time& delay) {
        tlm_command cmd = trans.get_command();
        sc_dt::uint64 addr = trans.get_address();
        unsigned char* ptr = trans.get_data_ptr();
        unsigned int len = trans.get_data_length();

        // Memory target only handles offsets 0x0000 - 0x03FF
        if (addr >= 1024) {
            trans.set_response_status(TLM_ADDRESS_ERROR_RESPONSE);
            return;
        }

        if (cmd == TLM_READ_COMMAND) {
            memcpy(ptr, &mem[addr], len);
        } else if (cmd == TLM_WRITE_COMMAND) {
            memcpy(&mem[addr], ptr, len);
        }

        // Apply a realistic memory access latency
        delay += sc_time(20, SC_NS);
        trans.set_response_status(TLM_OK_RESPONSE);
    }
};

// ---------------------------------------------------------
// Target 2: Peripheral (Base Address: 0x1000)
// ---------------------------------------------------------
class UARTPeripheral : public sc_module {
public:
    tlm_utils::simple_target_socket<UARTPeripheral> socket;

    SC_HAS_PROCESS(UARTPeripheral);
    UARTPeripheral(sc_module_name name) : sc_module(name), socket("socket") {
        socket.register_b_transport(this, &UARTPeripheral::b_transport);
    }

    void b_transport(tlm_generic_payload& trans, sc_time& delay) {
        tlm_command cmd = trans.get_command();
        unsigned char* ptr = trans.get_data_ptr();

        if (cmd == TLM_WRITE_COMMAND) {
            // Firmware writing to UART TX register
            std::cout << "[UART] Transmitting byte: 0x" << std::hex << (int)(*ptr) << std::dec << "\n";
        }

        // Peripheral access is slower than memory
        delay += sc_time(100, SC_NS);
        trans.set_response_status(TLM_OK_RESPONSE);
    }
};

// ---------------------------------------------------------
// Interconnect: Simple Router
// ---------------------------------------------------------
class Interconnect : public sc_module {
public:
    tlm_utils::simple_target_socket<Interconnect> target_socket;
    tlm_utils::simple_initiator_socket<Interconnect> init_socket_mem;
    tlm_utils::simple_initiator_socket<Interconnect> init_socket_uart;

    SC_HAS_PROCESS(Interconnect);
    Interconnect(sc_module_name name) : sc_module(name) {
        target_socket.register_b_transport(this, &Interconnect::b_transport);
    }

    void b_transport(tlm_generic_payload& trans, sc_time& delay) {
        sc_dt::uint64 addr = trans.get_address();

        // Standard address decoding map
        if (addr < 0x1000) {
            // Route to Memory
            init_socket_mem->b_transport(trans, delay);
        } else if (addr >= 0x1000 && addr < 0x2000) {
            // Route to UART and subtract base address for local offset
            trans.set_address(addr - 0x1000);
            init_socket_uart->b_transport(trans, delay);
            // Restore original address to maintain generic payload contract
            trans.set_address(addr);
        } else {
            trans.set_response_status(TLM_ADDRESS_ERROR_RESPONSE);
        }
    }
};

// ---------------------------------------------------------
// Initiator: Firmware CPU Wrapper
// ---------------------------------------------------------
class FirmwareCPU : public sc_module {
public:
    tlm_utils::simple_initiator_socket<FirmwareCPU> socket;

    SC_HAS_PROCESS(FirmwareCPU);
    FirmwareCPU(sc_module_name name) : sc_module(name), socket("socket") {
        SC_THREAD(execute_firmware);
    }

    void execute_firmware() {
        tlm_generic_payload trans;
        sc_time delay = SC_ZERO_TIME;
        unsigned char data;

        std::cout << "Time " << sc_time_stamp() << ": Firmware booting...\n";

        // 1. Read Boot Status from Memory (Address 0x0010)
        trans.set_command(TLM_READ_COMMAND);
        trans.set_address(0x0010);
        trans.set_data_ptr(&data);
        trans.set_data_length(1);
        trans.set_response_status(TLM_INCOMPLETE_RESPONSE);

        socket->b_transport(trans, delay);

        if (trans.get_response_status() == TLM_OK_RESPONSE) {
            std::cout << "Time " << sc_time_stamp() << ": Read boot status: 0x" << std::hex << (int)data << std::dec << " (Accumulated Delay: " << delay << ")\n";
        }

        // 2. Consume accumulated delay to sync with SystemC kernel
        wait(delay);
        delay = SC_ZERO_TIME;

        // 3. Write 'O' (0x4F) to UART (Address 0x1000)
        data = 0x4F;
        trans.set_command(TLM_WRITE_COMMAND);
        trans.set_address(0x1000);
        trans.set_response_status(TLM_INCOMPLETE_RESPONSE);

        socket->b_transport(trans, delay);
        wait(delay); // Sync again

        std::cout << "Time " << sc_time_stamp() << ": Firmware execution complete.\n";
    }
};

// ---------------------------------------------------------
// Top Level
// ---------------------------------------------------------
int sc_main(int argc, char* argv[]) {
    FirmwareCPU cpu("cpu");
    Interconnect bus("bus");
    MemoryTarget mem("mem");
    UARTPeripheral uart("uart");

    // Bindings
    cpu.socket.bind(bus.target_socket);
    bus.init_socket_mem.bind(mem.socket);
    bus.init_socket_uart.bind(uart.socket);

    sc_start();
    return 0;
}
```

## What This Architecture Demonstrates

A professional VP architect ensures the code communicates real hardware intent:
- **Address Decoding**: The `Interconnect` model acts as a router, stripping base addresses to provide the target with a 0-indexed local offset, matching real-world IP block behavior.
- **Timing Accumulation**: In Loosely Timed (LT) models, the initiator (`FirmwareCPU`) accumulates time in the `delay` variable during `b_transport` calls, but the SystemC kernel time `sc_time_stamp()` does not advance until `wait(delay)` is explicitly called. This enables blazing-fast simulation without sacrificing causality.
- **Payload Contracts**: Initiators must reset the response status to `TLM_INCOMPLETE_RESPONSE` before sending, and targets must set it to `TLM_OK_RESPONSE` (or an error) before returning.
- **Doulos / Standard Guidelines**: The use of `tlm_utils::simple_target_socket` cleanly encapsulates interface implementation boilerplate, matching the standard Accellera LT examples.

## Under the Hood: `simple_target_socket` C++ Implementation

When you use `tlm_utils::simple_target_socket`, you are bypassing the need to manually inherit from and implement the `tlm::tlm_fw_transport_if`.

In the official Accellera repository, `simple_target_socket` inherits from `tlm::tlm_target_socket<BUSWIDTH, TYPES>`. It encapsulates an internal nested class called `fw_process` that actively implements `b_transport`, `nb_transport_fw`, `get_direct_mem_ptr`, and `transport_dbg`.

When you call `socket.register_b_transport(this, &MemoryTarget::b_transport)`, the socket stores an `sc_core::sc_spawn_options` and a functor (or member function pointer adapter) inside its internal state.

During simulation, when the initiator calls `init_socket->b_transport(trans, delay)`, it traverses the bound SystemC port array and lands directly on the target's `fw_process::b_transport`. This internal method checks if a custom `b_transport` callback was registered. If yes, it dereferences the functor and executes your `MemoryTarget::b_transport` directly in the execution context of the initiator's thread. This abstraction provides a massive productivity boost while compiling down to zero-overhead C++ virtual function calls.

## Source-reading checkpoint

For firmware-visible configuration, inspect `.codex-src/cci` and the consuming broker path beside the bus model. Configuration only helps when its effect can be traced into runtime behavior.

## Lesson 118: VP Architecture Review for Technical Leads

Canonical lesson: https://www.learn-systemc.com/tutorials/113-vp-architecture-review-for-technical-leads

A lead-engineer checklist and compliant architectural example for reviewing a SystemC virtual platform before deployment.

## How to Read This Lesson

# VP Architecture Review for Technical Leads

This page is written for the technical lead who must decide whether a Virtual Platform (VP) is ready for firmware bring-up, architecture exploration, or customer release. A VP is only useful if it acts as a reliable, unambiguous contract between hardware designers and software engineers.

## Standard and source context

## The Review Checklist

Before a VP is deployed, it must be evaluated against the following criteria:

### 1. Memory Map Contract
Check that every region has a strictly defined:
- Base address and size (preventing overlaps and undefined holes).
- Access width policy and Endianness.
- Error response policy (what happens on unmapped accesses?).
- Debug transport support (`transport_dbg`).

### 2. Register Quality
If a firmware engineer cannot write a driver from the documentation and the model's behavior, the VP is incomplete. Every peripheral must enforce:
- Reset values and reserved bits.
- Read-only vs Write-only semantics.
- Side effects (e.g., clear-on-read).
- Interrupt generation policies.

### 3. Timing Contract
The VP must state exactly what simulation time means. Examples:
- "RAM access latency is modeled as a constant 20ns TLM delay."
- "UART transmission is one character delay per byte."
- "Interrupt propagation uses SystemC `sc_signal` update semantics."

### 4. Configuration and Observability
- CCI parameters must be named stably, documented, and properly locked if structural.
- The VP must detect unconsumed presets.
- Use standardized SystemC reporting macros (`SC_REPORT_INFO`, `SC_REPORT_FATAL`) instead of raw `std::cout`.

## Complete Example: The "Review-Ready" Compliant Block

The following complete, compilable example demonstrates a peripheral that strictly adheres to the review criteria above. It models a compliant timer peripheral with strict register semantics, debug transport, CCI configuration, proper memory bounds checking, and explicitly stated timing contracts.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>
#include <cci_configuration>

using namespace sc_core;
using namespace tlm;

class CompliantTimer : public sc_module {
public:
    // Memory map socket
    tlm_utils::simple_target_socket<CompliantTimer> socket;

    // Interrupt out
    sc_out<bool> irq_out;

    // CCI Configuration (Review #4)
    cci::cci_param<int> base_frequency_hz;

    SC_HAS_PROCESS(CompliantTimer);
    CompliantTimer(sc_module_name name)
        : sc_module(name)
        , socket("socket")
        , irq_out("irq_out")
        , base_frequency_hz("base_frequency_hz", 1000, "Base clock frequency in Hz")
    {
        socket.register_b_transport(this, &CompliantTimer::b_transport);
        socket.register_transport_dbg(this, &CompliantTimer::transport_dbg);

        SC_THREAD(timer_process);
    }

private:
    // Registers (Review #2: Register Quality)
    // 0x00: CTRL (Bit 0: Enable, Bit 1: Interrupt Enable)
    // 0x04: STATUS (Bit 0: Timer Fired - Clear on Write 1)
    uint32_t reg_ctrl = 0;
    uint32_t reg_status = 0;

    sc_event ev_timer_fired;

    // Review #3: Timing Contract.
    // Register access is modeled as a fixed 10ns delay.
    void b_transport(tlm_generic_payload& trans, sc_time& delay) {
        tlm_command cmd = trans.get_command();
        sc_dt::uint64 addr = trans.get_address();
        unsigned char* ptr = trans.get_data_ptr();
        unsigned int len = trans.get_data_length();

        // Review #1: Memory Map Contract (Bounds and access width checking)
        if (addr > 0x04 || len != 4) {
            trans.set_response_status(TLM_ADDRESS_ERROR_RESPONSE);
            return;
        }

        if (cmd == TLM_READ_COMMAND) {
            uint32_t val = (addr == 0x00) ? reg_ctrl : reg_status;
            memcpy(ptr, &val, 4);
        } else if (cmd == TLM_WRITE_COMMAND) {
            uint32_t val;
            memcpy(&val, ptr, 4);

            if (addr == 0x00) {
                reg_ctrl = val & 0x03; // Mask reserved bits
                SC_REPORT_INFO("Timer", "CTRL register updated.");
            } else if (addr == 0x04) {
                // Clear on write 1
                if (val & 0x01) {
                    reg_status &= ~0x01;
                    irq_out.write(false);
                }
            }
        }

        delay += sc_time(10, SC_NS); // Apply timing contract
        trans.set_response_status(TLM_OK_RESPONSE);
    }

    // Review #1 & #6: Debug transport bypasses delays and side-effects
    unsigned int transport_dbg(tlm_generic_payload& trans) {
        sc_dt::uint64 addr = trans.get_address();
        if (addr > 0x04 || trans.get_data_length() != 4) return 0;

        uint32_t val = (addr == 0x00) ? reg_ctrl : reg_status;
        memcpy(trans.get_data_ptr(), &val, 4);
        return 4; // Bytes read
    }

    void timer_process() {
        while (true) {
            // Wait for 1 tick based on CCI parameter
            sc_time tick_period(1.0 / base_frequency_hz.get_value(), SC_SEC);
            wait(tick_period);

            if (reg_ctrl & 0x01) { // If Enabled
                reg_status |= 0x01; // Set status

                if (reg_ctrl & 0x02) { // If Interrupt Enabled
                    irq_out.write(true);
                }
            }
        }
    }
};

// --- Top Level Testbench ---
int sc_main(int argc, char* argv[]) {
    // Setup Broker (Review #4)
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));

    // Instantiate and bind
    CompliantTimer timer("timer");
    sc_signal<bool> sig_irq;
    timer.irq_out(sig_irq);

    // Dummy transaction to test the contract
    tlm_generic_payload trans;
    sc_time delay = SC_ZERO_TIME;
    uint32_t data = 0x03; // Enable + IRQ Enable

    trans.set_command(TLM_WRITE_COMMAND);
    trans.set_address(0x00);
    trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
    trans.set_data_length(4);
    trans.set_response_status(TLM_INCOMPLETE_RESPONSE);

    // Send transaction (Simulating a CPU)
    timer.socket->b_transport(trans, delay);
    wait(delay); // Accumulate time

    if (trans.get_response_status() == TLM_OK_RESPONSE) {
        SC_REPORT_INFO("CPU", "Successfully configured timer.");
    }

    sc_start(2, SC_MS); // Run to see IRQ fire
    return 0;
}
```

## The Approval Bar

A Virtual Platform is considered "lead-review ready" only when:
- Software-visible behaviors (registers, interrupts) perfectly match documentation.
- Extraneous configuration parameters are locked.
- The boundary between Loosely Timed (LT) approximations and Cycle Accurate (CA) behavior is formally recorded.
- Debug inspection (`transport_dbg`) operates independently from time-advancing data paths.

## Under the Hood: `transport_dbg` and `sc_signal` Semantics

When reviewing a VP architecture, two C++ implementation details frequently cause subtle simulation bugs: `transport_dbg` violations and interrupt signal semantics.

**1. The `transport_dbg` Contract**
The `tlm_fw_transport_if` explicitly separates `b_transport` from `transport_dbg`. Under the hood, `transport_dbg` is a purely synchronous, non-blocking C++ function call that lacks an `sc_time` argument. It is illegal to call `sc_core::wait()` or modify the target's internal state (e.g., clearing a FIFO or a "clear-on-read" register bit) inside `transport_dbg`. Backdoor tools like GDB debuggers invoke `transport_dbg` directly through the interconnect. If it modifies state, the debugger observing memory will permanently corrupt the simulation timeline.

**2. Interrupt Propagation via `sc_signal`**
In the compliant example above, `irq_out.write(true)` is used. In the SystemC kernel, `sc_signal::write()` does not immediately change the signal's value. Instead, it schedules an `update()` request in the simulation kernel's event queue. The new value is applied during the *Update Phase* at the end of the current delta cycle.
If the CPU initiator models its interrupt polling within a single `b_transport` quantum without yielding back to the SystemC scheduler via `wait()`, it will completely miss the interrupt. VPs must explicitly state whether they use TLM payload interrupts (immediate execution) or `sc_signal` pins (requires a delta cycle yield).

## Lesson 119: VP Common Practices: Path Trace and Reporting Field Guide

Canonical lesson: https://www.learn-systemc.com/tutorials/137-vp-common-practices-path-trace-and-reporting-field-guide

Virtual-platform source practices for path tracing, report helpers, transaction strings, originators, and maintainable TLM utilities.

## How to Read This Lesson

Virtual platforms fail slowly when observability is weak. Path tracing, initiator IDs, report helpers, and transaction-to-string utilities are not decoration. They are the tooling that tells you which model did what, when, and why.

## Standard and source context

Use `.codex-src/systemc-common-practices` for the source examples. This is not an LRM, but it is valuable implementation guidance around reusable VP utilities.

## Path Trace

Path trace is the practice of carrying or reconstructing the route a transaction took through a platform: initiator, router, bridge, target, and any adapters in between.

In a small example, a transaction print may be enough. In a real SoC VP, path trace becomes how you answer, "did this access go through the expected interconnect path, or did a decoder route it somewhere surprising?"

The common-practices repository uses names around `tlm_extensions`, `initiator_id`, and `path_trace` to make that route explicit without changing the base `tlm_generic_payload`.

## Reporting Helpers

`scp_report`, `scp_logger_cache`, `scp_reporting_global`, `scp_global_originator`, `scp_smoke_report_test`, and report helper code sit beside the standard SystemC report mechanism.

The goal is consistency. If each block invents its own print format, platform debug becomes a text archaeology exercise. If reporting uses a common helper, severity, origin, and transaction context stay readable.

## Transaction String Utilities

`scp_txn_tostring` and similar helpers turn a payload into something a person can read: command, address, length, byte enables, streaming width, response status, extension summary, and initiator information.

This is not just for logs. A good transaction string is also useful in assertions, scoreboard errors, and trace snapshots.

## Runtime State Helpers

Names such as `sc_is_running`, `sc_stop_called`, `sc_get_current_object`, and `sc_verbosity` appear around runtime introspection and reporting behavior. They help utilities behave differently during elaboration, simulation, shutdown, or verbose debug.

Use these carefully. They are helpful for tools, but core model behavior should not become hard to reason about because it changes based on hidden global state.

## VP Review Checklist

- Can every transaction be tied back to an initiator?
- Can a route through the interconnect be reconstructed?
- Do report messages include hierarchy and enough transaction context?
- Are TLM extensions cloned and cleared correctly?
- Are utility helpers shared instead of duplicated in each peripheral?

## Source-reading checkpoint

For platform-wide tracing, inspect the common-practices register path, `transport_dbg`, and CCI broker touchpoints together. That gives reviewers one route through traffic, state, and diagnostics. Keep `tlm_target_socket`, `.codex-src/cci`, and `.codex-src/systemc-common-practices` in the review trail.

## Lesson 120: Modeling Best Practices: Abstraction

Canonical lesson: https://www.learn-systemc.com/tutorials/114-modeling-best-practices-abstraction

How to decide what to model, what to approximate, what to document, and how to keep SystemC examples production-like.

## How to Read This Lesson

# Modeling Best Practices: Abstraction

The most important SystemC skill is not knowing every API. It is choosing the right **abstraction level**. The IEEE 1666 LRM provides mechanisms ranging from bit-accurate, delta-cycle-level RTL modeling all the way up to Loosely Timed (LT) Transaction Level Modeling (TLM-2.0).

A good architect uses the simplest, fastest abstraction that still answers the system's design questions. We must look at the Accellera SystemC kernel's source code to understand *why* certain abstractions perform drastically better than others.

## Standard and source context

## The Spectrum of Abstraction

1. **Cycle-Accurate (RTL Level):** Uses `sc_signal`, `sc_logic`, clocks, and delta cycles.
   - *Kernel Reality:* Every clock edge triggers an `sc_event`. This places all sensitive `SC_METHOD`s into `sc_simcontext::m_runnable`. When `sc_signal::write()` is called, the kernel adds the signal to `sc_simcontext::m_update_list`. After execution, `sc_simcontext::crunch()` iterates the update list, triggering `m_value_changed_event`s, which schedules more processes, causing delta cycles. This heavy reliance on kernel data structures makes it slow.
   - *Pros:* Matches actual hardware perfectly.
   - *Cons:* Extremely slow simulation (KHz range).
   - *When to use:* Hardware validation, HLS (High-Level Synthesis) generation.

2. **Approximately Timed (AT) TLM:** Uses TLM-2.0 Non-Blocking Transport (`nb_transport`), modeling multiple protocol phases (Request, End-Request, Response) with annotated delays.
   - *Kernel Reality:* It avoids `sc_signal` and delta cycle updates, passing payloads via function calls. However, phases are synchronized using `tlm_peq_with_cb_and_phase` (Payload Event Queue), which internally relies on `sc_event::notify(sc_time)`. This still pushes `sc_event_timed` objects into `sc_simcontext::m_timed_events`, requiring kernel wake-ups, though much fewer than RTL.
   - *Pros:* Highly accurate bus contention and performance profiling.
   - *Cons:* Harder to write, moderate simulation speed.
   - *When to use:* Interconnect performance analysis, cache coherency studies.

3. **Loosely Timed (LT) TLM (Virtual Platforms):** Uses TLM-2.0 Blocking Transport (`b_transport`), temporal decoupling, and quantum keepers.
   - *Kernel Reality:* A `b_transport` is just a virtual C++ function call. A target module consumes time by adding to a local `sc_time delay` parameter rather than calling `wait()`. The thread is *temporally decoupled*, meaning it runs ahead of `sc_simcontext::m_curr_time` without yielding to the scheduler. No context switching (QuickThreads/pthreads) occurs, and `sc_simcontext::crunch()` is completely bypassed until the local time exceeds the TLM quantum.
   - *Pros:* Blistering fast simulation speed (100s of MHz). Capable of booting Linux in seconds.
   - *Cons:* Timing is approximate; cannot find race conditions on the bus.
   - *When to use:* Firmware development, early software bring-up, OS porting.

## Avoid Accidental RTL

If you are building a Virtual Platform (LT), avoid mixing RTL-style modeling inside it. If you model every clock edge of a UART transmitter using `sc_signal<bool>` and `wait()`, your entire platform's speed will be bottlenecked by that one UART.

Instead, model the UART at a high level: when software writes a byte, wait a bulk block of time (`wait(byte_delay)`), and then raise an interrupt.

## Complete Example: Abstracting a Timer

Here is a complete `sc_main` demonstrates the difference between an RTL-style timer (bad for VPs) and an abstract TLM-style timer (good for VPs).

```cpp
#include <systemc>
#include <iostream>

// ---------------------------------------------------------
// BAD for Virtual Platforms: Cycle-Accurate Timer (RTL Style)
// ---------------------------------------------------------
SC_MODULE(RtlTimer) {
    sc_core::sc_in<bool> clk{"clk"};
    sc_core::sc_out<bool> irq{"irq"};

    int counter = 0;
    int limit = 1000;

    SC_CTOR(RtlTimer) {
        SC_METHOD(tick);
        sensitive << clk.pos();
    }

    void tick() {
        // Wakes up on EVERY SINGLE CLOCK CYCLE! Very slow simulation.
        // Causes sc_simcontext to evaluate this method continuously.
        counter++;
        if (counter >= limit) {
            irq.write(true);
            counter = 0;
        } else {
            irq.write(false);
        }
    }
};

// ---------------------------------------------------------
// GOOD for Virtual Platforms: Abstract Timer (TLM Style)
// ---------------------------------------------------------
SC_MODULE(AbstractTimer) {
    sc_core::sc_event irq_event;
    sc_core::sc_time clock_period;
    int limit = 1000;

    SC_CTOR(AbstractTimer) : clock_period(10, sc_core::SC_NS) {
        SC_THREAD(timer_process);
    }

    void timer_process() {
        while (true) {
            // Wakes up ONLY when the interrupt is actually due!
            // Skips 1000 clock cycles instantly. High simulation speed.
            sc_core::sc_time wait_time = clock_period * limit;

            // Internally creates an sc_event_timed and pushes to m_timed_events
            // Yields the QuickThread coroutine context back to the scheduler.
            wait(wait_time);

            std::cout << "@ " << sc_core::sc_time_stamp()
                      << " [AbstractTimer] Interrupt Fired!\n";

            irq_event.notify(sc_core::SC_ZERO_TIME);
        }
    }
};

int sc_main(int argc, char* argv[]) {
    // We only simulate the AbstractTimer for this demonstration.
    AbstractTimer t_abs("abstract_timer");

    std::cout << "Starting Virtual Platform simulation...\n";
    sc_core::sc_start(25, sc_core::SC_US);

    return 0;
}
```

### Explanation of the Execution

```
Starting Virtual Platform simulation...
@ 10 us [AbstractTimer] Interrupt Fired!
@ 20 us [AbstractTimer] Interrupt Fired!
```

The `RtlTimer` would require the SystemC kernel to evaluate the `tick()` method 2,500 times to simulate 25 microseconds (assuming a 10ns clock). Under the hood, `sc_simcontext::crunch()` would loop 2,500 times, processing `m_runnable` and `m_update_list` continuously.

The `AbstractTimer` requires the SystemC kernel to evaluate `timer_process()` exactly **2 times**. By abstracting away the clock signal and calculating the bulk time jump, the thread process pushes an `sc_event_timed` precisely into the future and yields its stack via the `qt_block` assembly routine. The kernel's `m_curr_time` jumps directly to the target timestamp, bypassing 2,500 iterations. Simulation performance increases by orders of magnitude, making firmware development practical.

## Lesson 121: Modeling Best Practices: Datatypes and Performance

Canonical lesson: https://www.learn-systemc.com/tutorials/115-modeling-best-practices-datatypes-and-performance

Rules for using C++ types, SystemC datatypes, payload buffers, fixed-point values, and four-state logic in maintainable models.

## How to Read This Lesson

# Modeling Best Practices: Datatypes and Performance

Datatype choice is a critical architectural decision in SystemC. It directly impacts simulation speed, memory footprint, and the clarity of the C++ code. The IEEE 1666 standard provides powerful hardware-accurate types, but misusing them in software-oriented Virtual Platforms (VPs) is the leading cause of poor performance.

We must examine the Accellera kernel source code to understand the immense runtime cost of certain datatype selections.

## Standard and source context

## The Performance Hierarchy

When selecting a datatype, always start at the top of this hierarchy and only move down when the hardware semantics strictly demand it.

1. **Native C++ Types (`uint32_t`, `bool`, `std::array`):**
   - **Performance:** Native CPU speed.
   - **Kernel Reality:** Zero overhead. The compiler maps these directly to CPU registers and native load/store instructions.
   - **When to use:** Memory arrays, software-visible registers, counters, flags, TLM-2.0 payload data pointers.

2. **SystemC Fixed-Width Integers (`sc_dt::sc_uint<W>`, `sc_dt::sc_int<W>`):**
   - **Performance:** High.
   - **Kernel Reality:** Internally, an `sc_uint<W>` (where $W \le 64$) simply wraps a standard 64-bit integer (`uint64_t m_val`). The performance cost arises from proxy classes. When you call `.range(a,b)` or `operator[]`, SystemC instantiates `sc_uint_subref` or `sc_uint_bitref` proxy objects on the stack. These proxy classes overload `operator=` to execute bitwise masking (`&`, `|`, `<<`) under the hood to ensure exact hardware truncation.
   - **When to use:** Exact hardware bit-width modeling, register field extraction, bit-level concatenation where $W \le 64$.

3. **SystemC Arbitrary-Precision Integers (`sc_dt::sc_biguint<W>`):**
   - **Performance:** Slow.
   - **Kernel Reality:** Inherits from `sc_unsigned`. It represents large numbers as a heap-allocated array of `sc_digit` (which are typically `uint32_t` words). Every arithmetic operation involves `for`-loops iterating over these arrays to propagate carry bits. Temporary instantiations can trigger expensive heap memory allocations (`new[]`).
   - **When to use:** Cryptographic keys, very wide buses (e.g., 256-bit memory controllers).

4. **SystemC Bit Vectors (`sc_dt::sc_bv<W>`):**
   - **Performance:** Slower.
   - **Kernel Reality:** Internally backed by `sc_bv_base`. Bits are packed into an array of `sc_digit`s. While logic operations are bitwise, proxy overhead is severe for individual bit manipulation compared to native masking.
   - **When to use:** When you need to manipulate or observe uninterpreted streams of bits, but do not need 'X' or 'Z' states.

5. **SystemC Logic Vectors (`sc_dt::sc_lv<W>`, `sc_core::sc_logic`):**
   - **Performance:** Very Slow.
   - **Kernel Reality:** Implements 4-state logic ('0', '1', 'Z', 'X'). Under the hood, `sc_logic_vec` maintains two distinct `sc_digit` arrays: `m_data` (Data Plane) and `m_ctrl` (Control Plane). To resolve an operation, the kernel must execute parallel bitwise operations across both planes and utilize complex resolution tables.
   - **When to use:** Only for pin-level RTL interfaces where High-Impedance ('Z') or Unknown ('X') states are actively modeled and verified.

## TLM Payload Data and Endianness

TLM-2.0 generic payloads (`tlm_generic_payload`) transfer data using `unsigned char*`. Never cast this pointer directly to a C++ `struct` or a larger integer pointer (like `uint32_t*`) unless you are absolutely certain of the host machine's endianness and memory alignment rules.

Instead, construct the values explicitly.

## Complete Example: High-Performance Modeling

Here is a complete `sc_main` demonstrates the performance best practices: using native C++ arrays for memory, extracting bits correctly without proxy temporaries, and safely packing/unpacking TLM-style byte arrays.

```cpp
#include <systemc>
#include <iostream>
#include <vector>
#include <iomanip>

SC_MODULE(HighPerformanceMemory) {
    // 1. Native C++ type for large memory (Fast, low overhead)
    std::vector<uint8_t> ram;

    // 2. Hardware-accurate register for control logic
    sc_dt::sc_uint<32> status_register;

    SC_CTOR(HighPerformanceMemory) : ram(1024, 0), status_register(0) {
        SC_METHOD(run_tests);
    }

    // Helper function to safely read 32-bits from a byte array (Endian-safe)
    uint32_t read_le32(const uint8_t* p) {
        return uint32_t(p[0])
             | (uint32_t(p[1]) << 8)
             | (uint32_t(p[2]) << 16)
             | (uint32_t(p[3]) << 24);
    }

    // Helper function to safely write 32-bits to a byte array (Endian-safe)
    void write_le32(uint8_t* p, uint32_t val) {
        p[0] = static_cast<uint8_t>(val & 0xFF);
        p[1] = static_cast<uint8_t>((val >> 8) & 0xFF);
        p[2] = static_cast<uint8_t>((val >> 16) & 0xFF);
        p[3] = static_cast<uint8_t>((val >> 24) & 0xFF);
    }

    void run_tests() {
        // --- Test 1: TLM Payload Processing ---
        uint32_t test_val = 0xDEADBEEF;
        write_le32(&ram[0], test_val);

        uint32_t recovered = read_le32(&ram[0]);
        std::cout << "[Memory] Wrote: 0x" << std::hex << test_val
                  << " Recovered: 0x" << recovered << "\n";

        // --- Test 2: Bit Extraction without Proxy Overhead ---
        // BAD: status_register.range(15, 8) = ... (creates proxy temporaries)
        // GOOD: Use native operations where possible, or cast at boundaries

        uint32_t status_flags = 0x5; // Native
        sc_dt::sc_uint<4> hw_flags = status_flags; // Boundary conversion

        // Pack back into the status register safely via proxy objects
        // The sc_uint_subref proxy intercepts operator= and masks m_val
        status_register.range(3, 0) = hw_flags;
        status_register.range(31, 28) = 0xF;

        std::cout << "[Register] Status Reg: 0x" << status_register << "\n";
    }
};

int sc_main(int argc, char* argv[]) {
    HighPerformanceMemory mem("mem");

    std::cout << "Starting Simulation...\n";
    sc_core::sc_start();

    return 0;
}
```

### Explanation of the Execution

```
Starting Simulation...
[Memory] Wrote: 0xdeadbeef Recovered: 0xdeadbeef
[Register] Status Reg: 0xf0000005
```

By keeping the 1024-byte RAM as a `std::vector<uint8_t>`, the memory footprint is exactly 1KB, and reads/writes execute in a single CPU cycle. If `sc_dt::sc_lv<8>` were used for the RAM array instead, the memory footprint would skyrocket due to the complex class overhead of `m_data`/`m_ctrl` arrays for 4-state logic, and every read/write would require function calls to evaluate the logic tables.

The `read_le32` and `write_le32` functions guarantee that regardless of whether this code is compiled on an x86 (Little Endian) or ARM/PowerPC (potentially Big Endian) host, the modeled hardware behaves consistently as a Little Endian device.

## Lesson 122: Modeling Best Practices: API Docs and Doxygen

Canonical lesson: https://www.learn-systemc.com/tutorials/116-modeling-best-practices-api-docs-and-doxygen

How to comment SystemC modules, sockets, registers, CCI parameters, callbacks, and examples so generated docs help real users.

## How to Read This Lesson

# Modeling Best Practices: API Docs and Doxygen

A SystemC Virtual Platform is a software product. Like any software library, if the APIs are not documented, they are unusable. In SystemC, the "APIs" are your module constructors, TLM sockets, CCI parameters, and memory-mapped register contracts.

The industry standard for C++ documentation is **Doxygen**. Doxygen comments should explain *contracts and abstractions*, not just repeat the C++ syntax. Furthermore, Accellera provides programmatic ways to expose this metadata directly into the simulation kernel.

## Standard and source context

## What to Document and Expose to the Kernel

When distributing a SystemC IP block, the following elements MUST be documented, and where possible, registered with the Accellera kernel APIs:

- **Module Abstraction Level:** What does it model? (RTL, AT, LT/VP). What is intentionally left out?
- **TLM Sockets:** Which protocols do they support? Do they support DMI? What is the expected bus width?
- **Registers:** Base offsets, bitfields, reset values, and side-effects of reads/writes (e.g., "Reading this register clears the interrupt").
- **CCI Parameters (`cci_param`):** Name, type, default value, and mutability rules. When instantiating `cci_param`, you should also use `cci_param::set_description()` so that tools querying the `cci_broker_if` can extract the Doxygen string at runtime via the JSON-based `cci_value` AST.
- **Report Message Types (`msg_type`):** The string IDs used in `SC_REPORT_ERROR`. You should document these so integrators can configure the `sc_report_handler` to suppress or escalate specific warnings.

## Doxygen Commenting Style

Use the standard Doxygen `/** ... */` syntax.

### Module and Abstraction Comment
```cpp
/**
 * @class Uart
 * @brief Memory-mapped UART model for VP firmware bring-up.
 *
 * Models TX/RX FIFOs, status flags, and interrupt generation.
 * @note Bit-level serial waveform timing is intentionally abstracted.
 * Data is transferred instantaneously when the TX FIFO drains.
 */
class Uart : public sc_core::sc_module {
```

### TLM Socket Comment
```cpp
/**
 * @brief Target socket receiving memory-mapped register transactions.
 *
 * Supports standard TLM-2.0 b_transport. DMI is NOT supported for
 * memory-mapped peripheral registers. Expected payload width is 32 bits.
 */
tlm_utils::simple_target_socket<Uart> target_socket{"target_socket"};
```

### CCI Parameter Comment (with Kernel Registration)
```cpp
/**
 * @brief Approximate per-byte transmit delay.
 *
 * Mutable during simulation. Changing this affects future bytes only.
 * If set to SC_ZERO_TIME, the UART operates in zero-delay mode.
 */
cci::cci_param<sc_core::sc_time> tx_delay{"tx_delay", sc_core::sc_time(1, sc_core::SC_US)};

// Inside SC_CTOR, push the documentation to the CCI Broker:
// tx_delay.set_description("Approximate per-byte transmit delay. Mutable.");
```

## Complete Example: A Fully Documented IP Block

Here is a complete `sc_main` demonstrates how a professionally documented SystemC IP block should look. It includes Doxygen groupings, parameter documentation, and programmatic registration.

```cpp
#include <systemc>
#include <cci_configuration>
#include <iostream>

/**
 * @defgroup vp_timer Timer IP Block
 * @brief Abstract timer model for Loosely Timed (LT) Virtual Platforms.
 * @{
 */

/**
 * @class VpTimer
 * @brief A 32-bit countdown timer with interrupt generation.
 *
 * This model uses SystemC SC_THREADs to abstract away clock cycles.
 * It calculates the exact future time an interrupt should fire and waits
 * for that duration, maximizing simulation speed.
 */
SC_MODULE(VpTimer) {
    /**
     * @brief Interrupt output signal.
     * Active HIGH. Level-triggered.
     */
    sc_core::sc_out<bool> irq_out{"irq_out"};

    /**
     * @brief Frequency of the timer.
     * Accessible via cci_broker_if.
     */
    cci::cci_param<int> frequency_hz{"frequency_hz", 1000000};

    /**
     * @name Register Offsets
     * Memory map offsets relative to the module base address.
     * @{
     */
    static constexpr uint32_t REG_CTRL  = 0x00; ///< Control register. Bit 0: Enable.
    static constexpr uint32_t REG_LIMIT = 0x04; ///< Value to countdown from.
    static constexpr uint32_t REG_ACK   = 0x08; ///< Write any value to clear IRQ.
    /** @} */

    /**
     * @brief Constructs the VpTimer.
     * @param name The SystemC hierarchical name.
     */
    SC_CTOR(VpTimer) {
        // Register the documentation string with the Accellera CCI broker
        frequency_hz.set_description("Clock frequency of the timer in Hz.");

        SC_THREAD(timer_process);
    }

private:
    void timer_process() {
        wait(10, sc_core::SC_NS); // Dummy logic for compilation
        irq_out.write(true);
    }
};

/** @} */ // End vp_timer group

int sc_main(int argc, char* argv[]) {
    // Instantiate the documented IP block
    sc_core::sc_signal<bool> irq_sig{"irq_sig"};
    VpTimer timer("my_timer");
    timer.irq_out(irq_sig);

    // Query the CCI Broker to demonstrate runtime metadata extraction
    cci::cci_broker_handle broker = cci::cci_get_broker();
    cci::cci_param_handle param = broker.get_param_handle("my_timer.frequency_hz");

    std::cout << "Runtime Param Description: " << param.get_description() << "\n";

    std::cout << "Starting Simulation of Documented IP...\n";
    sc_core::sc_start(1, sc_core::SC_US);

    return 0;
}
```

### Explanation of the Execution

```
Runtime Param Description: Clock frequency of the timer in Hz.
Starting Simulation of Documented IP...
```

While running this code executes the simulation logic, running the `doxygen` tool against this source file will generate a professional HTML manual.

By combining static Doxygen comments with runtime `cci_broker_if` metadata (via `set_description()`), your models become fully introspectable. A firmware engineer can read the generated HTML to find that `REG_ACK = 0x08`, while an automated VP configuration tool can query the `cci_broker_if` at runtime to generate a GUI tooltip for the `frequency_hz` parameter.

## Lesson 123: Modeling Best Practices: Expert Code Review Checklist

Canonical lesson: https://www.learn-systemc.com/tutorials/117-modeling-best-practices-expert-code-review-checkli

A senior SystemC review checklist for kernel semantics, TLM correctness, CCI configuration, AMS boundaries, UVM usage, and documentation.

## How to Read This Lesson

# Modeling Best Practices: Expert Code Review Checklist

When reviewing production SystemC code, "it compiles and simulates" is not enough. SystemC's flexibility means poor architectural choices might simulate correctly today but deadlock tomorrow under a different kernel scheduler order, or cripple the performance of an entire SoC integration.

Use this LRM-compliant checklist to evaluate IP blocks before they are merged into a shared library. We will include the Accellera kernel source rationale for *why* these rules exist.

## Standard and source context

## 1. Core Kernel Semantics

**Rule:** Ensure predictable scheduling and memory safety.
- **Hierarchy Lifetimes:** No `sc_object` (ports, modules, signals) is created as a local stack variable inside a constructor.
  - *Kernel Reason:* The `sc_object_manager` links every child `sc_object` into a dynamically managed tree via `m_hierarchy_curr`. If an object is allocated on the stack, it is destroyed when the constructor exits, leaving a dangling pointer in the kernel's object tree. `sc_object` derivatives must be allocated via `new` or be class members.
- **Elaboration Phase:** Ports and exports are fully bound before `end_of_elaboration`.
- **Method Restrictions:** `SC_METHOD` processes must *never* call `wait()`.
  - *Kernel Reason:* `SC_METHOD` processes are executed directly by `sc_simcontext::crunch()` on the OS's primary call stack. They do not have an allocated coroutine stack (QuickThreads or pthreads). If you call `wait()`, the kernel catches this via `sc_get_curr_process_handle()->process_kind()` and throws `SC_ID_WAIT_NOT_ALLOWED_` because there is no context to yield.
- **Thread Yielding:** `SC_THREAD` processes must contain a `wait()` statement inside their infinite loop (otherwise the simulation hangs forever).
- **Writer Policies:** If `SC_MANY_WRITERS` is used on a signal, is it justified?
  - *Kernel Reason:* In `sc_signal::write()`, the kernel aggressively throws an exception if multiple distinct processes call `write()` in the same evaluation phase, ensuring deterministic behavior. Disabling this bypasses hardware realism checks.

## 2. TLM-2.0 Correctness

**Rule:** Ensure strict adherence to the TLM-2.0 protocol phases.
- **Response Status:** Every target socket MUST set a response status (`set_response_status()`) before returning from a blocking transport call.
- **Memory Management:** Does the module safely handle `tlm_generic_payload` memory management (`tlm_mm_interface`)?
  - *Kernel Reason:* Allocating payloads via `new` is incredibly slow. The TLM-2.0 standard requires initiators to acquire payloads from a memory pool, call `acquire()`, pass them to targets, and upon return, call `release()`. The target must not retain pointers to `get_data_ptr()` after the transaction ends without acquiring a reference lock.
- **Unsupported Commands:** If a target doesn't support `TLM_WRITE_COMMAND`, does it correctly return `TLM_COMMAND_ERROR_RESPONSE` instead of silently ignoring it?
- **Data Length:** Does the target validate `get_data_length()` against its internal register sizes to prevent buffer overflows (segfaults)?
- **Debug Transport:** Does `transport_dbg` guarantee zero side-effects? (It must never advance time, clear interrupts, or pop FIFOs).

## 3. Configuration & Virtual Platform Quality

**Rule:** Ensure the IP is reusable and configurable by a top-level architect.
- **Parameters:** Are magic numbers replaced by CCI-compliant parameters (`cci::cci_param`)?
- **Abstraction:** Is the timing abstraction clear? (e.g., "This timer fires accurately, but register read delays are assumed to be 0").
- **Reporting:** Do `SC_REPORT` messages use a hierarchical `msg_type` (e.g., `"/VENDOR/IP/ERROR"`)? Do error messages include context (Address, value, internal state)?
  - *Kernel Reason:* `sc_report_handler` uses string matching to selectively suppress, log, or stop simulation based on these `msg_type` strings. Generic strings like "Error" break the ability to filter.

## Complete Example: The Reviewer's Sandbox

Here is a complete `sc_main` acts as a "Reviewer's Sandbox", demonstrating a module that passes the checklist (Good IP) and a module that fails several checks (Bad IP).

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>
#include <iostream>

// ---------------------------------------------------------
// BAD IP: Fails the Code Review
// ---------------------------------------------------------
SC_MODULE(BadIP) {
    tlm_utils::simple_target_socket<BadIP> socket{"socket"};

    SC_CTOR(BadIP) {
        socket.register_b_transport(this, &BadIP::b_transport);
        // Fails Checklist: SC_METHOD calling wait()!
        // Kernel will throw an SC_ID_WAIT_NOT_ALLOWED_ exception.
        // SC_METHOD(bad_method);
    }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        // Fails Checklist: Ignores command type!
        // Fails Checklist: Ignores byte enables!
        // Fails Checklist: NEVER SETS RESPONSE STATUS! (Initiator will panic)
        std::cout << "[BadIP] Received transaction.\n";
    }
};

// ---------------------------------------------------------
// GOOD IP: Passes the Code Review
// ---------------------------------------------------------
SC_MODULE(GoodIP) {
    tlm_utils::simple_target_socket<GoodIP> socket{"socket"};
    uint32_t internal_reg;

    SC_CTOR(GoodIP) : internal_reg(0) {
        socket.register_b_transport(this, &GoodIP::b_transport);
    }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        tlm::tlm_command cmd = trans.get_command();
        unsigned char*   ptr = trans.get_data_ptr();
        unsigned int     len = trans.get_data_length();
        unsigned char*   byt = trans.get_byte_enable_ptr();

        // Passes Checklist: Validates Byte Enables
        if (byt != nullptr) {
            trans.set_response_status(tlm::TLM_BYTE_ENABLE_ERROR_RESPONSE);
            return;
        }

        // Passes Checklist: Validates Data Length
        if (len != 4) {
            trans.set_response_status(tlm::TLM_BURST_ERROR_RESPONSE);
            return;
        }

        // Passes Checklist: Handles Commands properly
        if (cmd == tlm::TLM_READ_COMMAND) {
            memcpy(ptr, &internal_reg, 4);
        } else if (cmd == tlm::TLM_WRITE_COMMAND) {
            memcpy(&internal_reg, ptr, 4);
        }

        // Passes Checklist: Sets Successful Response Status
        trans.set_response_status(tlm::TLM_OK_RESPONSE);

        // Annotate abstract delay
        delay += sc_core::sc_time(10, sc_core::SC_NS);

        std::cout << "[GoodIP] Successfully processed "
                  << (cmd == tlm::TLM_READ_COMMAND ? "READ" : "WRITE") << ".\n";
    }
};

int sc_main(int argc, char* argv[]) {
    GoodIP good("good_ip");
    BadIP bad("bad_ip");

    // We don't simulate here as the sockets are unconnected,
    // but this code compiles and demonstrates the architectural differences.
    std::cout << "Code Review Sandbox Compiled Successfully.\n";

    return 0;
}
```

### Explanation of the Execution

The `BadIP` module is a prime example of code that might compile but violates the TLM-2.0 standard. If an initiator sends a payload to `BadIP`, the initiator's check of `trans.get_response_status()` will show `TLM_INCOMPLETE_RESPONSE`, causing the simulation to abort or fail a verification check.

The `GoodIP` module defensively checks inputs, handles the payload safely, annotates timing accurately, and guarantees a response status is set. This is the quality expected of production virtual platform IP.

## Source-reading checkpoint

For a review, use `.codex-src/cci` and `cci_param_if` as a concrete checkpoint: parameters need ownership, lifecycle rules, and a reason to remain configurable.

## Lesson 124: Modeling Best Practices: Debugging Playbook

Canonical lesson: https://www.learn-systemc.com/tutorials/118-modeling-best-practices-debugging-playbook

How experts debug SystemC failures by separating elaboration, scheduling, binding, timing, payload, and configuration problems.

## How to Read This Lesson

# Modeling Best Practices: Debugging Playbook

SystemC failures often look mysterious because C++ execution, kernel scheduling, and modeled hardware behavior are heavily interleaved. A segmentation fault could be a C++ pointer error, or it could be a TLM payload mismatch. A simulation hang could be an infinite loop, or it could be a delta-cycle deadlock.

Expert debugging requires classifying the failure first, then applying the correct tools and examining the correct kernel data structures in your debugger.

## Standard and source context

## Class 1: Elaboration and Binding Failures

**Symptoms:** Exceptions thrown before `sc_start()` begins; messages about unbound ports.
**Cause:** The LRM requires all ports and exports to be bound before simulation starts.
**Playbook:**
- Use the LRM API `sc_core::sc_get_top_level_objects()` to print the hierarchy.
- In GDB, set a breakpoint on `sc_core::sc_port_base::complete_binding()`.
- Ensure no modules were instantiated as local stack variables inside constructors. This breaks `sc_object_manager::m_hierarchy_curr`, corrupting the kernel's object tree.
- Check that multi-ports (`sc_port<IF, N>`) have the correct number of bindings.

## Class 2: Scheduling Failures (Hangs and Deadlocks)

**Symptoms:** Simulation time (`sc_time_stamp()`) stops advancing, but CPU usage is 100%. Or, simulation freezes entirely.
**Cause:**
- *CPU 100%:* A delta-cycle loop. Process A triggers B, B triggers A, all in `SC_ZERO_TIME`.
- *Freeze:* An `SC_THREAD` forgot to call `wait()`, locking the cooperative scheduler forever.
**Playbook:**
- If frozen, attach GDB and inspect `sc_core::sc_get_curr_simcontext()->m_runnable`. If it's empty, and the current frame is inside user code rather than `sc_simcontext::crunch()`, a thread forgot to yield (via `wait()` and the `qt_block` assembly context switch).
- If spinning at 100% CPU, inspect `sc_simcontext::m_update_list` and `sc_simcontext::m_delta_events`. A delta loop will continuously populate these. Use the LRM API `sc_core::sc_delta_count()` to print the delta cycle count. If it increases while time stands still, you found the loop.

## Class 3: TLM-2.0 Protocol Failures

**Symptoms:** Firmware reads garbage data, routers forward to the wrong target, or an initiator throws an `TLM_INCOMPLETE_RESPONSE` error.
**Cause:** Violation of the TLM-2.0 base protocol.
**Playbook:**
- In GDB, break on your `b_transport` function and inspect the `tlm_generic_payload` object. Check the `m_address`, `m_command`, and `m_response_status` fields.
- Verify the Target modifies `set_response_status()`.
- If using DMI (Direct Memory Interface), disable it temporarily. If the bug disappears, your DMI invalidation logic is flawed. DMI bypasses TLM sockets entirely, making memory overwrites completely invisible to standard socket debugging.

## Complete Example: Debugging a Delta Cycle Loop

Here is a complete `sc_main` demonstrates a classic delta-cycle loop (Class 2 failure) and how to use the LRM APIs (`sc_delta_count`) to detect and debug it programmatically.

```cpp
#include <systemc>
#include <iostream>

SC_MODULE(DeltaLoopDemo) {
    sc_core::sc_signal<bool> sig_a{"sig_a"};
    sc_core::sc_signal<bool> sig_b{"sig_b"};

    SC_CTOR(DeltaLoopDemo) {
        SC_METHOD(process_a);
        sensitive << sig_b; // A reacts to B

        SC_METHOD(process_b);
        sensitive << sig_a; // B reacts to A

        SC_METHOD(monitor_deltas);
        sensitive << sig_a << sig_b;
    }

    void process_a() {
        // Invert B and write to A
        // Kernel adds sig_a to sc_simcontext::m_update_list
        sig_a.write(!sig_b.read());
    }

    void process_b() {
        // Invert A and write to B
        // Kernel adds sig_b to sc_simcontext::m_update_list
        sig_b.write(!sig_a.read());
    }

    void monitor_deltas() {
        // The LRM provides sc_delta_count() to track evaluation phases
        uint64_t current_delta = sc_core::sc_delta_count();
        std::cout << "[Time: " << sc_core::sc_time_stamp()
                  << "] Delta Count: " << current_delta << "\n";

        // Safeguard to prevent an actual infinite loop in this demonstration
        if (current_delta > 10) {
            SC_REPORT_ERROR("DEBUG", "Delta cycle loop detected! Aborting.");
        }
    }
};

int sc_main(int argc, char* argv[]) {
    // Configure the report handler to stop instead of abort for the demo
    sc_core::sc_report_handler::set_actions(sc_core::SC_ERROR, sc_core::SC_DISPLAY | sc_core::SC_STOP);

    DeltaLoopDemo demo("demo");

    std::cout << "Starting Simulation... Watch the delta count explode.\n";

    // Kickoff the loop
    demo.sig_b.write(true);

    sc_core::sc_start(1, sc_core::SC_MS);

    std::cout << "Simulation stopped cleanly after detecting the loop.\n";
    return 0;
}
```

### Explanation of the Execution

When you run this code, `process_a` writes to `sig_a`. In the update phase, `sig_a` changes, triggering `process_b`. `process_b` writes to `sig_b`. In the next update phase, `sig_b` changes, triggering `process_a`.

Because signal updates take zero simulation time, `sc_time_stamp()` remains at `0 s`, but the kernel's `crunch()` function evaluates continuously.

The output will look like this:
```
Starting Simulation... Watch the delta count explode.
[Time: 0 s] Delta Count: 1
[Time: 0 s] Delta Count: 2
[Time: 0 s] Delta Count: 3
...
[Time: 0 s] Delta Count: 11
Error: (E0000) DEBUG: Delta cycle loop detected! Aborting.
Simulation stopped cleanly after detecting the loop.
```

By printing `sc_time_stamp()` alongside `sc_delta_count()`, the architect instantly diagnoses a combinatorial feedback loop rather than wondering why the simulation froze.

## Lesson 125: Modeling Best Practices: Public API Contracts

Canonical lesson: https://www.learn-systemc.com/tutorials/119-modeling-best-practices-public-api-contracts

How to define stable contracts for reusable SystemC models: sockets, ports, registers, parameters, reports, traces, and ownership.

## How to Read This Lesson

# Modeling Best Practices: Public API Contracts

A SystemC model used by other teams has an API even if nobody calls it a library. If you change a parameter name, a report ID, or a TLM socket behavior, you break the build or the simulations of downstream users.

To write industrial-grade Virtual Platform (VP) models, you must define and stabilize your **Public API Contracts**. Let us examine how these contracts map directly to Accellera kernel structures.

## Standard and source context

## The Contract Surfaces

In a professional SystemC IP block, the following elements constitute the public contract and must not be broken or changed without version bumping:

- **Module Hierarchy Names:** Do not use `sc_gen_unique_name()` for top-level objects.
  - *Kernel Reality:* The kernel maintains a global hash table in `sc_object_manager` mapping hierarchical string paths to `sc_object*`. Automated configuration tools (via CCI) rely on these paths. Generating unique names dynamically breaks reproducible configurations.
- **Ports and Exports:** Ensure the data types and interface types are completely stable.
- **TLM Sockets:** Which payload extensions are required? What byte enables are supported?
- **Register Map:** Base offsets, bitfields, and reset behaviors.
- **CCI Parameters:** Hierarchical paths, metadata, default values, and mutability.
  - *Kernel Reality:* These are registered in the singleton `cci_broker_if`. If a parameter name changes, top-level JSON configurations loaded by the broker will fail to apply.
- **Report IDs (`msg_type`):** The exact strings used in `SC_REPORT_WARNING` and `SC_REPORT_ERROR`.
  - *Kernel Reality:* The `sc_report_handler` maintains a rule map comparing the exact `msg_type` string to decide whether to `SC_DISPLAY`, `SC_LOG`, or `SC_STOP`. Breaking the string breaks the user's simulation filters.
- **DMI / Debug Transport:** Whether the model safely supports backdoor memory access without side-effects.

## Socket Contracts

For each TLM socket, your Doxygen or Markdown documentation must answer:

- What happens if `get_byte_enable_ptr() != nullptr`?
- Are `TLM_IGNORE_COMMAND` payloads handled gracefully?
- What is the expected streaming width?
- If it is an AT (Approximately Timed) target, which protocol phases does it actively use?
- **Memory Management:** For `b_transport` and `nb_transport`, does the target adhere to `tlm_mm_interface` reference counting? For `transport_dbg`, does it safely bypass `acquire()` and `release()`? (Debug transactions do not use the memory pool).

## Complete Example: Designing a Contract-Safe IP Block

Here is a complete `sc_main` demonstrates an IP block that treats its external surface as a strict contract, validating inputs safely and exposing a clean, documented hierarchy.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>
#include <iostream>

// ---------------------------------------------------------
// IP BLOCK WITH STRICT CONTRACTS
// ---------------------------------------------------------
class ContractSafeIP : public sc_core::sc_module {
public:
    // Contract 1: Stable Socket Name
    tlm_utils::simple_target_socket<ContractSafeIP> target_socket{"target_socket"};

    // Contract 2: Stable Port Name
    sc_core::sc_out<bool> interrupt_out{"interrupt_out"};

    // Contract 3: Documented Register Map
    static constexpr uint64_t REG_STATUS = 0x00;
    static constexpr uint64_t REG_DATA   = 0x04;

    SC_HAS_PROCESS(ContractSafeIP);

    // Contract 4: Predictable Constructor
    ContractSafeIP(const sc_core::sc_module_name& name)
        : sc_core::sc_module(name), internal_data(0) {

        // Register standard blocking transport callback
        target_socket.register_b_transport(this, &ContractSafeIP::b_transport);

        // Register standard debug transport callback (No side-effects!)
        target_socket.register_transport_dbg(this, &ContractSafeIP::transport_dbg);
    }

private:
    uint32_t internal_data;

    // --- Blocking Transport (Functional Behavior) ---
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        tlm::tlm_command cmd = trans.get_command();
        uint64_t         adr = trans.get_address();
        unsigned int     len = trans.get_data_length();
        unsigned char*   byt = trans.get_byte_enable_ptr();

        // Contract Enforcement: No byte enables supported
        if (byt != nullptr) {
            trans.set_response_status(tlm::TLM_BYTE_ENABLE_ERROR_RESPONSE);
            return;
        }

        // Contract Enforcement: Must be 32-bit access
        if (len != 4) {
            trans.set_response_status(tlm::TLM_BURST_ERROR_RESPONSE);
            return;
        }

        // Functional side-effects happen here
        if (cmd == tlm::TLM_WRITE_COMMAND && adr == REG_DATA) {
            memcpy(&internal_data, trans.get_data_ptr(), 4);
            interrupt_out.write(true); // Side-effect!

            // Contract 5: Stable Report ID.
            // The sc_report_handler uses this exact string to filter output.
            SC_REPORT_INFO("IP_BLOCK/WRITE", "Data register updated, interrupt asserted.");
        }
        else if (cmd == tlm::TLM_READ_COMMAND && adr == REG_DATA) {
            memcpy(trans.get_data_ptr(), &internal_data, 4);
            interrupt_out.write(false); // Side-effect!
        }
        else {
            trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
            return;
        }

        trans.set_response_status(tlm::TLM_OK_RESPONSE);
        delay += sc_core::sc_time(10, sc_core::SC_NS);
    }

    // --- Debug Transport (No Side Effects!) ---
    unsigned int transport_dbg(tlm::tlm_generic_payload& trans) {
        if (trans.get_command() == tlm::TLM_READ_COMMAND && trans.get_address() == REG_DATA) {
            if (trans.get_data_length() >= 4) {
                memcpy(trans.get_data_ptr(), &internal_data, 4);
                // Notice: We do NOT clear the interrupt here! Debug is invisible.
                // We also do not call trans.acquire() or release() for debug payloads.
                return 4;
            }
        }
        return 0;
    }
};

// ---------------------------------------------------------
// TESTBENCH
// ---------------------------------------------------------
int sc_main(int argc, char* argv[]) {
    sc_core::sc_signal<bool> irq{"irq"};
    ContractSafeIP ip_inst("ip_inst");
    ip_inst.interrupt_out(irq);

    // Dummy payload for testing
    tlm::tlm_generic_payload trans;
    sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
    uint32_t data = 0xABCD;

    trans.set_command(tlm::TLM_WRITE_COMMAND);
    trans.set_address(ContractSafeIP::REG_DATA);
    trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
    trans.set_data_length(4);
    trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

    std::cout << "Sending TLM Write...\n";
    ip_inst.target_socket->b_transport(trans, delay);

    if (trans.is_response_ok()) {
        std::cout << "Write Successful. IRQ State: " << irq.read() << "\n";
    }

    return 0;
}
```

### Explanation of the Execution

```
Sending TLM Write...
Info: (I804) /IEEE_Std_1666/main: IP_BLOCK/WRITE: Data register updated, interrupt asserted.
Write Successful. IRQ State: 1
```

By explicitly checking byte enables, length, and addresses, the IP block guarantees it will not crash with a segmentation fault if an integrator misuses it. By separating `b_transport` from `transport_dbg`, it allows software debuggers (GDB connected to the VP) to inspect memory without accidentally triggering hardware state machines.

This level of rigor is what transforms "academic SystemC" into "industrial SystemC".

## Lesson 126: Modeling Best Practices: Source and Standard Traceability

Canonical lesson: https://www.learn-systemc.com/tutorials/120-modeling-best-practices-source-and-standard-tracea

How to connect model behavior to LRM rules, Accellera implementation files, examples, tests, and project documentation.

## How to Read This Lesson

# Modeling Best Practices: Source and Standard Traceability

For senior engineers, "because it works in my simulator" is not enough. SystemC models must outlive the specific compiler and simulator version they were written against.

Important model behavior should be explicitly traceable to a standard rule (IEEE 1666 LRM), an implementation detail (Accellera PoC GitHub repositories), a specific test, or a documented internal project policy.

## Standard and source context

## The Four Traceability Levels

When commenting code or discussing architectural decisions, classify the justification into one of these four levels:

1. **Standard:** The behavior is mandated by the IEEE 1666 LRM. (e.g., "Ports must be bound before `end_of_elaboration`"). This code is 100% portable across all commercial simulators.
2. **Implementation:** The behavior is derived from reading the Accellera open-source codebase (e.g., `sysc/kernel/sc_simcontext.cpp`). (e.g., "The Accellera kernel uses `sc_pq` (a priority queue) for `m_timed_events`, making future event insertion O(log N). However, it uses a simple `std::vector` for `m_update_list`, making `request_update()` extremely fast"). This code is mostly portable, but relies on implementation details that an aggressive commercial simulator might change.
3. **Project Policy:** The behavior is a team convention. (e.g., "All our virtual platforms use native `uint32_t` for memory rather than `sc_bv<32>` for performance").
4. **Tool Workaround:** The behavior exists to bypass a bug in a specific tool or compiler. These should always be marked with a `TODO` or issue tracker link so they can be deleted later.

## Example: Signal Update Semantics

**Standard Traceability:**
> *IEEE 1666 Section 6.4: The `sc_signal::write()` method shall submit an update request. The new value shall not be visible to readers until the update phase of the current delta cycle.*

**Kernel Source Traceability:**
> *In `sysc/communication/sc_signal.cpp`, `write()` stores the value in `m_new_val` and calls `request_update()`. The kernel pushes `this` pointer to `sc_simcontext::m_update_list`. Later, `sc_simcontext::crunch()` iterates the list and calls the virtual `update()` method, which finally commits `m_new_val` to `m_cur_val` and notifies `m_value_changed_event`.*

**Project Policy Translation:**
> *To prevent non-deterministic combinatorial loops, no IP block in this project shall attempt to read an `sc_signal` in the same evaluation phase it was written.*

## Complete Example: Documenting Traceable Code

Here is a complete `sc_main` demonstrates how to write a SystemC module where every architectural decision is documented with its traceability level. This is the gold standard for industrial Virtual Platforms.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>
#include <iostream>

SC_MODULE(TraceableIP) {
    // [Project Policy]: Sockets must use snake_case and end with '_socket'
    tlm_utils::simple_target_socket<TraceableIP> target_socket{"target_socket"};

    // [Standard]: SC_HAS_PROCESS is required when not using SC_CTOR.
    // Reference: IEEE 1666 Section 5.2.7
    SC_HAS_PROCESS(TraceableIP);

    TraceableIP(const sc_core::sc_module_name& name) : sc_core::sc_module(name) {
        target_socket.register_b_transport(this, &TraceableIP::b_transport);
    }

    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        // [Standard]: Target MUST set response status before returning.
        // Reference: IEEE 1666 Section 14.12
        trans.set_response_status(tlm::TLM_OK_RESPONSE);

        // [Implementation]: We accumulate delay locally to leverage the
        // Accellera TLM quantum keeper optimization, reducing context switches.
        // Under the hood, this avoids pushing an sc_event_timed to the kernel's m_timed_events queue.
        delay += sc_core::sc_time(10, sc_core::SC_NS);

        // [Tool Workaround]: Simulator X crashes if we print payload pointers directly,
        // so we cast to void* first. (Ticket: #1234)
        std::cout << "[TraceableIP] Handled payload at ptr: "
                  << static_cast<void*>(&trans) << "\n";
    }
};

int sc_main(int argc, char* argv[]) {
    // [Project Policy]: All top-level signals must be explicitly named
    sc_core::sc_signal<bool> rst_n{"rst_n"};

    TraceableIP ip("ip_inst");

    // Dummy test payload
    tlm::tlm_generic_payload trans;
    sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
    trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

    ip.target_socket->b_transport(trans, delay);

    return 0;
}
```

### Why Traceability Matters

If a commercial simulator vendor updates their engine and your simulation suddenly hangs, traceability saves weeks of debugging.

If your comments cite the LRM (*Level 1*), you can file a bug against the vendor. If your comments rely on an Accellera implementation detail (*Level 2*), you know immediately that you wrote non-portable code and need to fix your architecture. If you used a Tool Workaround (*Level 4*), you know you can likely delete the workaround now that the tool was updated.

## Lesson 127: LRM Semantic Conventions and Portability

Canonical lesson: https://www.learn-systemc.com/tutorials/121-lrm-semantic-conventions-and-portability

How to read shall, should, may, implementation-defined behavior, errors, transient objects, and portability boundaries in SystemC standards.

## How to Read This Lesson

This lesson is for the moments when two experienced engineers argue about whether a behavior is guaranteed, merely allowed, or just what one simulator happens to do. The LRM has a vocabulary for that. Learn it once and your reviews become much calmer.

## Standard and source context

Use `Docs/LRMs/SystemC_LRM_1666-2023.pdf` for the semantic conventions around required behavior, implementation-defined behavior, non-compliant applications, errors, and returned references or pointers. The same style of language appears in the AMS, CCI, UVM-SystemC, and synthesis LRMs. Source examples live in `Accellera SystemC GitHub repository/systemc`, `Accellera SystemC GitHub repository/cci`, and `Accellera SystemC GitHub repository/uvm-systemc`, but source behavior is not automatically a portability guarantee.

## Why This Matters

SystemC code often runs on several simulators: the Accellera proof-of-concept library, commercial simulators, vendor HLS tools, and internal platform frameworks. If your model depends on a behavior that the LRM does not require, it may pass today and fail during tool migration.

So before you write a clever workaround, classify the behavior:

- **Required by the LRM:** portable model behavior.
- **Implementation-defined:** portable only after you check and document the implementation.
- **Unspecified or undefined:** do not build architecture around it.
- **Accellera PoC detail:** useful for debugging and learning, but not a public contract.
- **Project policy:** valid inside your organization, but document it as local guidance.

## Reading "Shall", "Should", "May", and "Can"

When the LRM says an implementation **shall** do something, your model can rely on it. When it says an application **shall** do something, your code must obey that rule to be compliant.

When the LRM says **should**, it is a recommendation. A tool may still be compliant if it behaves differently, but you should have a strong reason before ignoring it.

When it says **may**, the standard allows an option. This is where portability review matters. If an implementation may choose behavior A or B, your model should not silently require A unless you constrain the toolchain.

When it says **can**, the text is usually explanatory: it tells you what is possible, not what is required.

## Implementation-Defined Is Not a Loophole

Implementation-defined behavior means the implementation must define what it does. It does not mean the behavior is universal.

A common example is diagnostic detail. The standard may require an error to be reported, but the exact wording, message ID, stack shape, or recovery action can be implementation-specific.

Good project documentation says:

```cpp
// Portable rule: unbound mandatory ports are an elaboration error.
// Tool note: our regression parser recognizes Accellera message IDs,
// but the model must not depend on the exact message text.
```

That comment separates the standard rule from the local automation.

## Returned References and Transient Objects

Many SystemC APIs return references or pointers. The LRM carefully describes when those objects are stable and when they are transient.

The practical rule is simple: do not store references to temporary lists, event expressions, or objects whose lifetime is tied to a single expression unless the API explicitly promises that lifetime.

This pattern is safe:

```cpp
SC_METHOD(on_event);
sensitive << a.value_changed_event();
```

This pattern deserves review:

```cpp
const sc_core::sc_event_or_list& events = a.default_event() | b.default_event();
```

The expression creates helper objects used to describe dynamic sensitivity. Storing a reference to such expression machinery is exactly the sort of lifetime mistake semantic conventions are warning you about.

## Source Insight: Why the Rule Exists

In the Accellera implementation, event expressions and sensitivity helpers are represented by C++ objects that help the kernel build sensitivity lists. Some are persistent events; others are temporary expression wrappers. The source makes the standard's warning feel practical: C++ object lifetime rules still apply, even when the syntax looks like hardware notation.

## Review Checklist

- Is this behavior explicitly required by the LRM?
- If not, is it implementation-defined, unspecified, or merely observed in Accellera source?
- Are comments clear about which category the behavior belongs to?
- Does the code store references or pointers returned by APIs with unclear lifetime?
- Would this model still work on a different compliant simulator?

## Lesson 128: Version-Specific Notes for the SystemC Family

Canonical lesson: https://www.learn-systemc.com/tutorials/122-version-specific-notes-for-the-systemc-family

The SystemC, TLM, AMS, CCI, UVM-SystemC, synthesis, and common-practices versions used by the lessons.

## How to Read This Lesson

This is a practical version note for the whole course: when a lesson says "SystemC", "CCI", or "UVM-SystemC", this page tells you which version baseline the explanation is using.

## Standard and source context

The baselines come from the local LRMs in `Docs/LRMs/`, the official Accellera repositories listed in `Docs/LRMs/SourceWebsite.txt`, and the public Accellera release pages checked by `scripts/check_accellera_updates.py`.

## Version Baseline Used in the Lessons

| Area | Baseline taught in the site | Where the implementation discussion points |
| --- | --- | --- |
| SystemC core | SystemC 3.0.2 and IEEE 1666-2023 | `Accellera SystemC GitHub repository` |
| TLM-2.0 | Included with SystemC 3.0.2 and IEEE 1666-2023 | `Accellera SystemC GitHub repository` |
| SystemC AMS | SystemC AMS 2.0 LRM; AMS release artifacts monitored separately | AMS LRM plus SystemC DE synchronization lessons |
| SystemC CCI | CCI 1.0 LRM and CCI 1.0.1 proof-of-concept source | `Accellera SystemC GitHub repository/cci/configuration/src/cci` |
| UVM-SystemC | UVM-SystemC 1.0-beta6 public-review release signal; the local February 2023 draft LRM remains the clause-reading baseline until the newer manual is added | `Accellera SystemC GitHub repository/uvm-systemc/src/uvmsc` |
| SystemC Synthesis Subset | SystemC Synthesis Subset 1.4.7 LRM | synthesis subset lessons and HLS review rules |
| SystemC Common Practices | Accellera common-practices repository checkout | `Accellera SystemC GitHub repository/systemc-common-practices` |

## How Version Notes Should Be Used

If a class, function, macro, or source path changes upstream, the lesson that teaches that feature should get the version-specific note directly in the lesson. For example:

- a scheduler behavior update belongs in the scheduler lesson
- a socket implementation change belongs in the TLM socket internals lesson
- a new CCI mutability rule belongs in the CCI parameter lifecycle lesson
- a new UVM-SystemC phase behavior belongs in the phase lesson

The version note should explain:

1. what changed
2. which version introduced or clarified it
3. whether it is a portable LRM change or an Accellera implementation change
4. whether model code needs to change

## Automated Upstream Check

The repository includes `scripts/check_accellera_updates.py`. It checks public Accellera release pages and the official GitHub repositories, then writes `Docs/Audits/UPSTREAM_VERSION_CHECK.md`.

The checker does not rewrite lessons automatically. That is deliberate. Version changes in standards need human judgment: sometimes a release changes implementation files but not portable behavior; sometimes an LRM clarification changes how a best-practice page should be worded.

## UVM-SystemC Draft Note

Accellera's public-review page lists UVM-SystemC `1.0-beta6`, published in July 2024. The local `Docs/LRMs/uvm-systemc-language-reference-manual.pdf` file is the February 2023 draft associated with the earlier public-review generation. Read the local document for the currently checked-in clause text, inspect the official `accellera-official/uvm-systemc` repository for implementation details, and verify beta6 differences before teaching a beta6-specific API or behavior.

## Lesson 129: Introduction to High-Level Synthesis (HLS)

Canonical lesson: https://www.learn-systemc.com/tutorials/123-introduction-to-high-level-synthesis-hls

An overview of High-Level Synthesis and the SystemC Synthesizable Subset.

## How to Read This Lesson

# Introduction to High-Level Synthesis (HLS)

The SystemC Synthesizable Subset (often referenced via the Accellera SystemC Synthesis Working Group) defines the subset of the SystemC C++ class library that can be reliably synthesized into register-transfer level (RTL) hardware by High-Level Synthesis (HLS) tools.

While SystemC is an incredibly powerful language for simulation and virtual prototyping, not everything you can write in C++ can be physically built in silicon. For example, dynamically allocating memory on the heap using `new` or `malloc` during the middle of a hardware simulation has no physical equivalent in a fixed silicon area.

## Standard and source context

## The Goal of the Synthesizable Subset

The goal of the synthesis subset is to provide a standardized, common denominator of SystemC constructs that EDA vendors (like Cadence, Synopsys, Siemens, etc.) agree upon. If you write your SystemC code using only these constructs, you are guaranteed that an HLS tool can compile your C++ code into Verilog or VHDL.

## Key Restrictions

To make C++ synthesizable, several major restrictions are enforced:

1.  **No Dynamic Allocation After Elaboration:** You cannot dynamically allocate memory or objects after the `start_of_simulation()` phase. All hardware must be statically known.
2.  **No Dynamic Processes:** Functions like `sc_spawn` are generally not synthesizable. You must use static `SC_METHOD` or `SC_CTHREAD` registrations.
3.  **Restricted Pointers:** Pointer arithmetic is heavily restricted or outright banned. Pointers can only be used if the HLS tool can statically determine exactly what they point to at compile time.
4.  **No Recursion:** Hardware cannot easily implement arbitrary recursion without an infinite unbounded stack. Recursive function calls are not synthesizable.
5.  **Standard Library Limits:** Standard C++ library features like `std::vector`, `std::map`, or file I/O (`std::cout`, `fstream`) are meant for simulation only and cannot be synthesized into silicon.

In the next tutorial, we will explore which specific SystemC datatypes are supported for synthesis.

## Under the Hood: Synthesizable C++ Constraints
High-Level Synthesis (HLS) tools parse SystemC using an LLVM-based frontend, but they ignore the SystemC simulation scheduler. They statically analyze the Abstract Syntax Tree (AST).
Because hardware is statically allocated, you cannot use `new` or `malloc` after the elaboration phase. `std::vector` with dynamic sizing will cause synthesis failure because the tool cannot determine the number of physical flip-flops to instantiate.
Furthermore, recursion and function pointers are generally forbidden because the call graph must be fully unrolled into physical logic paths. When an HLS tool sees an `sc_module`, it treats it as a Verilog `module`, converting its `sc_in`/`sc_out` directly into physical ports.

## Lesson 130: Synthesizable Datatypes

Canonical lesson: https://www.learn-systemc.com/tutorials/124-synthesizable-datatypes

Understanding which SystemC datatypes are supported by HLS tools.

## How to Read This Lesson

# Synthesizable Datatypes

When targeting High-Level Synthesis (HLS), choosing the right datatype is critical. In hardware, every bit costs area, power, and routing resources. Using a 32-bit `int` to store a value that only goes from 0 to 5 is a massive waste of silicon.

The SystemC Synthesizable Subset strictly defines which types are allowed and encouraged.

## Standard and source context

## Supported SystemC Types

The following SystemC-specific types are fully supported for synthesis:

*   **`sc_int<W>` and `sc_uint<W>`:** Limited precision (1 to 64 bits) integer types. These are the workhorses of HLS because they synthesize extremely efficiently.
*   **`sc_bigint<W>` and `sc_biguint<W>`:** Arbitrary precision integer types (greater than 64 bits). These are synthesizable, but arithmetic operations on very wide buses will result in massive logic gates and slow clock speeds.
*   **`sc_logic` and `sc_bv<W>`:** Bit-vector and logic types. These are supported, but modern HLS flows often prefer `sc_uint` for arithmetic.
*   **Fixed Point Types (`sc_fixed`, `sc_ufixed`):** Fully synthesizable and highly recommended for DSP applications where floating-point math is too expensive.

## Native C++ Types

Native C++ integer types are synthesizable, but their hardware size depends on the compiler standard (usually 32 bits for `int`).

*   `bool` (synthesizes to a 1-bit wire or flip-flop)
*   `char`, `short`, `int`, `long`, `long long` (and their unsigned variants).

> [!WARNING] Floating Point
> `float` and `double` are technically synthesizable by modern advanced HLS tools, but they instantiate massive, slow IEEE-754 compliant floating-point arithmetic units (FPU). Unless you specifically intend to build a hardware FPU, use `sc_fixed` instead.

## Unsupported Types

*   **`std::string`:** Strings cannot be synthesized. Hardware does not process dynamic text.
*   **STL Containers:** `std::vector`, `std::list`, `std::map` rely on dynamic heap allocation. They are completely unsynthesizable.
*   **Pointers to functions:** Not supported.

When writing synthesizable SystemC, always constrain your bit-widths. Use `sc_uint<3>` for a counter that counts to 7, rather than a generic `int`.

## Under the Hood: `sc_int` and `sc_dt::sc_uint_base`
Standard C++ `int` is typically 32-bit. SystemC provides arbitrary precision types like `sc_int<W>` and `sc_biguint<W>`.
In `sysc/datatypes/int/sc_uint_base.h`, an `sc_uint<W>` (where `W <= 64`) is simply stored as a standard `uint64_t`. The class overloads the mathematical and bitwise operators to apply a bit-mask (`m_mask`) after every operation, ensuring the value wraps around according to the custom bit-width `W`.
For `sc_biguint<W>` (`W > 64`), the value is stored as an array of 32-bit integers (`sc_digit`). Operations on big types require loops over these arrays with carry propagation, which is why they are significantly slower to simulate than native `uint64_t`.

## Source-reading checkpoint

For synthesis-oriented datatype choices, inspect the `sc_cthread`, reset, and `wait` examples beside the subset LRM. The source view is useful only after the hardware intent is clear.

## Lesson 131: Processes: SC_METHOD vs SC_CTHREAD

Canonical lesson: https://www.learn-systemc.com/tutorials/125-processes-sc-method-vs-sc-cthread

How SystemC processes are interpreted by High-Level Synthesis tools.

## How to Read This Lesson

# Processes: SC_METHOD vs SC_CTHREAD

In SystemC simulation, you have three process types: `SC_METHOD`, `SC_THREAD`, and `SC_CTHREAD`. When it comes to High-Level Synthesis (HLS), this list is strictly narrowed.

## Standard and source context

## The Ban on SC_THREAD

The standard `SC_THREAD` is generally **not** recommended for synthesis, and many strict HLS tools outright reject it.

Why? An `SC_THREAD` is an unconstrained coroutine. It can call `wait()` on any arbitrary event, at any time, with no relationship to a clock. Hardware requires a clock to schedule state transitions. Synthesizing an unconstrained `SC_THREAD` into a Finite State Machine (FSM) is extremely difficult and ambiguous.

## Using SC_CTHREAD and SC_METHOD

Instead of `SC_THREAD`, HLS tools mandate the use of `SC_CTHREAD` (Clocked Thread) for sequential logic, and `SC_METHOD` for purely combinational logic.

An `SC_CTHREAD` is statically bound to a single clock edge during elaboration. When you call `wait()` inside an `SC_CTHREAD`, the HLS tool knows exactly what it means: "Wait exactly one clock cycle." This allows the HLS compiler to automatically extract a Finite State Machine (FSM).

An `SC_METHOD` must not have any internal state (no `static` variables) and must write to all its outputs in every possible execution path to avoid inferring latches.

Here is a complete, fully compilable example demonstrating both synthesizable process types:

```cpp
#include <systemc>

using namespace sc_core;

SC_MODULE(HLS_Process_Demo) {
    sc_in_clk clk{"clk"};
    sc_in<bool> reset{"reset"};

    // Inputs and Outputs for Combinational Logic
    sc_in<int> a{"a"};
    sc_in<int> b{"b"};
    sc_out<int> max_val{"max_val"};

    // Output for Sequential Logic (FSM)
    sc_out<int> state_out{"state_out"};

    // 1. SC_CTHREAD for sequential FSM logic
    void clocked_logic() {
        // Reset state
        state_out.write(0);
        wait(); // Wait 1 clock cycle

        while (true) {
            // State 1
            state_out.write(1);
            wait(); // Wait 1 clock cycle

            // State 2
            state_out.write(2);
            wait(); // Wait 1 clock cycle
        }
    }

    // 2. SC_METHOD for purely combinational logic
    void combinational_logic() {
        // If a > b, out = a, else out = b (Combinational Mux)
        if (a.read() > b.read()) {
            max_val.write(a.read());
        } else {
            max_val.write(b.read());
        }
    }

    SC_CTOR(HLS_Process_Demo) {
        SC_CTHREAD(clocked_logic, clk.pos());
        reset_signal_is(reset, true); // Synchronous reset declaration

        SC_METHOD(combinational_logic);
        sensitive << a << b; // Sensitive to inputs, exactly like combinational hardware
    }
};

int sc_main(int argc, char* argv[]) {
    sc_clock clk("clk", 10, SC_NS);
    sc_signal<bool> reset("reset");
    sc_signal<int> a("a"), b("b"), max_val("max_val"), state_out("state_out");

    HLS_Process_Demo demo("demo");
    demo.clk(clk);
    demo.reset(reset);
    demo.a(a);
    demo.b(b);
    demo.max_val(max_val);
    demo.state_out(state_out);

    sc_start(50, SC_NS);
    return 0;
}
```

For synthesis, `SC_METHOD` is sensitive to changes in its inputs, exactly as it is in simulation. By using `SC_CTHREAD` for sequential logic and `SC_METHOD` for combinational logic, you guarantee your code remains safely within the synthesizable subset.

## Under the Hood: `SC_CTHREAD` and Clock Boundaries
While `SC_THREAD` is standard in SystemC, `SC_CTHREAD` is heavily preferred in HLS.
In `sysc/kernel/sc_cthread_process.cpp`, an `SC_CTHREAD` is explicitly tied to the edge of a clock signal (`sensitive << clk.pos()`).
For HLS tools, a call to `wait()` inside an `SC_CTHREAD` represents a distinct clock boundary (a state in the generated Finite State Machine). The logic between two `wait()` calls is synthesized as the combinational logic that computes the next state and outputs for that specific cycle.

## Lesson 132: Ports, Interfaces, and Hardware Boundaries

Canonical lesson: https://www.learn-systemc.com/tutorials/126-ports-interfaces-and-hardware-boundaries

How HLS interprets SystemC ports and pins at the hardware boundary.

## How to Read This Lesson

# Ports, Interfaces, and Hardware Boundaries

When you write a SystemC module for simulation, ports are just pointers to channels. However, when you pass that SystemC module to an HLS tool to generate RTL (Verilog/VHDL), those ports become physical wires and pins on a silicon chip.

## Standard and source context

## Pin-Level Interfaces

The most basic and universally supported synthesizable ports are `sc_in<T>` and `sc_out<T>`. When an HLS tool sees `sc_in<sc_uint<8>> data_in;`, it generates an 8-bit wide input wire bus on the Verilog module.

### Clocks and Resets

Clocks and resets must be explicitly identified. HLS tools use these to schedule the Finite State Machine (FSM).
*   `sc_in_clk` (or `sc_in<bool>`) is used for clocks.
*   You must use `reset_signal_is()` during the `SC_CTOR` to explicitly tell the HLS tool which port is the reset, and whether it is active-high or active-low.

## Array of Ports and HLS

Arrays of ports are synthesizable as long as the array size is a constant, statically determinable integer. Dynamically allocated arrays of ports using `sc_vector` are supported by modern HLS tools only if the vector size is completely fixed and known during elaboration.

Here is a complete compilable example demonstrating static port arrays and explicit reset binding:

```cpp
#include <systemc>
#include <sysc/datatypes/int/sc_uint.h>

using namespace sc_core;

SC_MODULE(PortArrayDemo) {
    sc_in_clk clk{"clk"};
    sc_in<bool> rst{"rst"};

    // Supported: Generates 4 distinct 8-bit input buses in RTL
    sc_in<sc_dt::sc_uint<8>> data_bus[4];
    sc_out<sc_dt::sc_uint<10>> sum_out{"sum_out"};

    SC_CTOR(PortArrayDemo) {
        SC_CTHREAD(compute_sum, clk.pos());
        reset_signal_is(rst, true);
    }

    void compute_sum() {
        sum_out.write(0);
        wait();

        while (true) {
            sc_dt::sc_uint<10> temp_sum = 0;
            // Static loops can be unrolled by HLS tools
            for (int i = 0; i < 4; ++i) {
                temp_sum += data_bus[i].read();
            }
            sum_out.write(temp_sum);
            wait();
        }
    }
};

int sc_main(int argc, char* argv[]) {
    sc_clock clk("clk", 10, SC_NS);
    sc_signal<bool> rst("rst");
    sc_signal<sc_dt::sc_uint<8>> bus_signals[4];
    sc_signal<sc_dt::sc_uint<10>> sum_signal("sum_signal");

    PortArrayDemo demo("demo");
    demo.clk(clk);
    demo.rst(rst);
    demo.sum_out(sum_signal);

    for (int i = 0; i < 4; ++i) {
        demo.data_bus[i](bus_signals[i]);
    }

    sc_start(50, SC_NS);
    return 0;
}
```

## Custom Channels and Interfaces

In the SystemC simulation world, you can write custom hierarchical channels that implement complex interfaces.

In the Synthesis world, this is heavily restricted. Most HLS tools require all module boundaries to eventually resolve down to standard pin-level ports (`sc_in`, `sc_out`). You cannot easily pass a complex C++ object or a generic custom interface across a physical hardware boundary without explicitly defining the wire protocol.

To get around this, modern HLS tools have introduced specialized synthesis pragmas or compiler directives that instruct the tool on how to map a C++ function call into a hardware bus protocol (like AXI4).

In the next tutorial, we will discuss the holy grail of system-level design: Synthesizing TLM-2.0.

## Under the Hood: HLS Port Mapping
In pure SystemC, an `sc_in<T>` is just an `sc_port` bound to an `sc_signal`.
However, an HLS compiler maps these ports to specific hardware protocols using pragmas or attributes. For example, if you synthesize a function with `sc_in<int>`, the tool might map it to a simple wire. If you use `sc_fifo_in<T>`, the HLS tool automatically synthesizes the `valid`, `ready`, and `data` handshake signals of an AXI4-Stream interface. The semantics of `read()` on the FIFO are translated into an FSM state that halts execution until `valid == 1` and asserts `ready = 1`.

## Lesson 133: Synthesizing TLM-2.0

Canonical lesson: https://www.learn-systemc.com/tutorials/127-synthesizing-tlm-2-0

The challenges and solutions for High-Level Synthesis of TLM-2.0 models.

## How to Read This Lesson

# Synthesizing TLM-2.0

Transaction Level Modeling (TLM) is the cornerstone of Virtual Prototyping. However, synthesizing TLM-2.0 models into physical hardware represents one of the most advanced and difficult frontiers in EDA (Electronic Design Automation).

## Standard and source context

## Why is TLM-2.0 Hard to Synthesize?

Standard TLM-2.0 relies on several concepts that are fundamentally incompatible with basic hardware generation:

1.  **Pointers and Dynamic Payloads:** `tlm_generic_payload` is usually allocated on the heap, passed by pointer, and contains pointers to data buffers. Hardware cannot easily pass memory pointers across physical bus wires.
2.  **Function Calls Across Boundaries:** `b_transport` is a blocking function call. In simulation, module A calls a function that executes inside module B's context. In hardware, module A and module B are separate physical blocks of silicon.
3.  **Timing Annotations:** The `sc_time` delay passed in TLM-2.0 is meant for loosely-timed simulation. Hardware operates on physical clock cycles.

## Synthesizable TLM Subsets

To solve this, EDA vendors provide **Synthesizable TLM subsets** or specialized libraries. When you call `b_transport()` in a synthesizable TLM model:
1.  The HLS tool recognizes the TLM socket.
2.  It halts the caller's `SC_CTHREAD`.
3.  It drives the physical address, data, and control pins of the generated AXI bus.
4.  It waits for the AXI `READY` signal.
5.  It resumes the `SC_CTHREAD` when the transaction completes.

Here is a complete compilable example demonstrating the structural pattern for a synthesizable TLM-2.0 Initiator. It uses static allocation for the payload and relies on vendor-agnostic C++ pragmas which are ignored by standard simulators but picked up by HLS tools.

```cpp
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>

using namespace sc_core;

SC_MODULE(SynthesizableInitiator) {
    sc_in_clk clk{"clk"};
    sc_in<bool> rst{"rst"};
    tlm_utils::simple_initiator_socket<SynthesizableInitiator> init_socket{"init_socket"};

    SC_CTOR(SynthesizableInitiator) {
        SC_CTHREAD(run_fsm, clk.pos());
        reset_signal_is(rst, true);
    }

    void run_fsm() {
        wait(); // Reset synchronization

        while (true) {
            // Static payload for synthesis (No 'new' operator)
            tlm::tlm_generic_payload trans;
            unsigned char data[4] = {0xAA, 0xBB, 0xCC, 0xDD};

            trans.set_command(tlm::TLM_WRITE_COMMAND);
            trans.set_address(0x4000);
            trans.set_data_ptr(data);
            trans.set_data_length(4);
            trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

            sc_time delay = SC_ZERO_TIME;

            // HLS tools often map this directly to an AXI master interface
            #pragma HLS interface m_axi port=init_socket
            init_socket->b_transport(trans, delay);

            if (trans.is_response_error()) {
                SC_REPORT_ERROR(name(), "TLM Write Failed");
            }
            wait();
        }
    }
};

SC_MODULE(DummyTarget) {
    tlm_utils::simple_target_socket<DummyTarget> target_socket{"target_socket"};
    SC_CTOR(DummyTarget) {
        target_socket.register_b_transport(this, &DummyTarget::b_transport);
    }
    void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
    }
};

int sc_main(int argc, char* argv[]) {
    sc_clock clk("clk", 10, SC_NS);
    sc_signal<bool> rst("rst");

    SynthesizableInitiator init("init");
    DummyTarget tgt("tgt");

    init.clk(clk);
    init.rst(rst);
    init.init_socket.bind(tgt.target_socket);

    rst.write(true);
    sc_start(10, SC_NS);
    rst.write(false);
    sc_start(50, SC_NS);

    return 0;
}
```

## Best Practices for Synthesizable TLM

*   **Avoid DMI (Direct Memory Interface):** DMI relies entirely on raw C++ pointers mapping directly to host memory. This cannot be synthesized.
*   **Keep Data Static:** Ensure the data arrays you pass into the generic payload are statically sized and locally allocated within the module, as shown above.
*   **Vendor-Specific Pragmas:** You will almost certainly need to annotate your TLM sockets with `#pragma HLS` (or equivalent) to tell the compiler which physical hardware bus to generate.

By following the SystemC Synthesizable Subset, engineers can write an algorithm once in C++, verify it in a Virtual Platform at high speed, and then push that exact same source code through an HLS compiler to generate physical silicon.

## Under the Hood: TLM Synthesis Limitations
TLM-2.0 `tlm_generic_payload` relies heavily on dynamic memory allocation, pointers, and virtual functionsâ€”all of which are notoriously difficult or impossible to synthesize into RTL.
When you attempt to synthesize TLM interfaces, HLS tools often require strict constraints: payloads must be statically allocated (e.g., placed on the stack), and extension pointers are usually forbidden. Some vendors provide synthesized wrappers (like `tlm_fifo`) that map a subset of TLM blocking transport calls directly into AXI4 Memory Mapped bus protocols.

## Lesson 134: Resets in High-Level Synthesis (Synchronous vs. Asynchronous)

Canonical lesson: https://www.learn-systemc.com/tutorials/128-resets-in-high-level-synthesis-synchronous-vs-asyn

A deep dive into how High-Level Synthesis (HLS) models and generates synchronous and asynchronous resets from SystemC code according to the Synthesis Subset LRM.

## How to Read This Lesson

# Resets in High-Level Synthesis (HLS)

In digital design, resets are critical for bringing your hardware into a known, predictable initial state. In standard C++ software, you initialize variables in a constructor. In hardware, however, state-holding elements (like flip-flops and registers) require a physical reset routing network.

When writing SystemC for High-Level Synthesis (HLS), the **SystemC Synthesis Subset LRM** explicitly defines how resets must be modeled so that the HLS compiler can correctly map them to physical asynchronous or synchronous reset pins in RTL (Verilog/VHDL).

## Standard and source context

## The Anatomy of an HLS Process

In HLS, hardware blocks are predominantly modeled using `SC_CTHREAD` (Clocked Threads) or clocked `SC_METHOD`s.

To make a thread synthesizable with a reset, you must strictly follow a specific coding pattern:
1. **The Reset Block (Initialization):** The code immediately following the start of the function, up to the first `wait()`, is considered the *reset block*. This defines the default state of all variables and outputs when the reset signal is active.
2. **The Functional Block (Infinite Loop):** An infinite `while(true)` loop follows the reset block. This represents the actual operational hardware logic that executes on every clock cycle when the reset is *not* active.

> [!WARNING]
> If you omit the initial `wait()` after your reset assignments, or if you place logic before the `while(true)` loop that takes multiple cycles, most HLS tools will reject the code or synthesize it incorrectly.

## The Kernel Reality: Exception Unwinding

SystemC provides specific registration macros to tell the simulation kernel (and the HLS compiler) how a reset behaves. But how does a thread jump out of an infinite loop back to the top of its function?

The Accellera kernel implements this using **C++ exceptions**. When a reset condition is triggered, the kernel invokes `sc_process_b::reset_process()`, which throws an internal `sc_unwind_exception` inside your coroutine. This abruptly unwinds the call stack out of the `while(true)` loop, catches it in the kernel's process runner, and restarts the thread function from line 1.

*   **`async_reset_signal_is(port, active_level)`**: The reset is asynchronous. The kernel actively monitors this signal independently of the clock. If the active level is hit, the kernel interrupts the `SC_CTHREAD` immediately, throws the unwind exception, and executes the reset block in the current delta cycle.
*   **`sync_reset_signal_is(port, active_level)`**: The reset is synchronous. The kernel only evaluates the reset signal when the thread wakes up due to its static sensitivity (e.g., `clk.pos()`). If the reset is active, it throws the unwind exception on the clock edge.
*   *Legacy Note:* The older `reset_signal_is()` macro is generally interpreted as synchronous by default, but modern IEEE 1666 standard practices prefer the explicit `async_` and `sync_` variants for clarity.

## End-to-End Example: Modeling Resets

Below is a complete, compilable SystemC model demonstrating both an active-low asynchronous reset and an active-high synchronous reset in the same module.

```cpp
#include <systemc.h>

// -------------------------------------------------------------------------
// Synthesizable Hardware Module
// -------------------------------------------------------------------------
SC_MODULE(ResetDemo) {
    // Inputs
    sc_in<bool> clk;
    sc_in<bool> rst_async_n; // Active-low asynchronous reset
    sc_in<bool> rst_sync;    // Active-high synchronous reset
    sc_in<int>  data_in;

    // Outputs
    sc_out<int> data_out_async;
    sc_out<int> data_out_sync;

    // SC_CTHREAD modeling asynchronous reset
    void async_reset_thread() {
        // --- RESET BLOCK ---
        // This executes immediately when rst_async_n goes low.
        data_out_async.write(0);
        wait(); // REQUIRED: Boundary between reset and functional logic

        // --- FUNCTIONAL BLOCK ---
        while (true) {
            // Read input, add 1, and drive output
            data_out_async.write(data_in.read() + 1);

            // If rst_async_n goes low while waiting here,
            // the kernel throws an sc_unwind_exception and restarts the thread!
            wait();
        }
    }

    // SC_CTHREAD modeling synchronous reset
    void sync_reset_thread() {
        // --- RESET BLOCK ---
        // This executes on the clock edge only if rst_sync is high.
        data_out_sync.write(0);
        wait(); // REQUIRED: Boundary between reset and functional logic

        // --- FUNCTIONAL BLOCK ---
        while (true) {
            // Read input, add 2, and drive output
            data_out_sync.write(data_in.read() + 2);
            wait(); // Wait for the next rising clock edge
        }
    }

    SC_CTOR(ResetDemo) {
        // Register the asynchronous thread
        SC_CTHREAD(async_reset_thread, clk.pos());
        // Tell the tool: rst_async_n is an async reset, active when false (low)
        async_reset_signal_is(rst_async_n, false);

        // Register the synchronous thread
        SC_CTHREAD(sync_reset_thread, clk.pos());
        // Tell the tool: rst_sync is a sync reset, active when true (high)
        sync_reset_signal_is(rst_sync, true);
    }
};

// -------------------------------------------------------------------------
// Testbench / Simulation
// -------------------------------------------------------------------------
int sc_main(int argc, char* argv[]) {
    // Signals to wire up the DUT
    sc_clock clk("clk", 10, SC_NS);
    sc_signal<bool> rst_async_n;
    sc_signal<bool> rst_sync;
    sc_signal<int> data_in;
    sc_signal<int> data_out_async;
    sc_signal<int> data_out_sync;

    // Instantiate and bind
    ResetDemo dut("dut");
    dut.clk(clk);
    dut.rst_async_n(rst_async_n);
    dut.rst_sync(rst_sync);
    dut.data_in(data_in);
    dut.data_out_async(data_out_async);
    dut.data_out_sync(data_out_sync);

    // Setup waveform tracing for debugging
    sc_trace_file* tf = sc_create_vcd_trace_file("reset_waveforms");
    tf->set_time_unit(1, SC_NS);
    sc_trace(tf, clk, "clk");
    sc_trace(tf, rst_async_n, "rst_async_n");
    sc_trace(tf, rst_sync, "rst_sync");
    sc_trace(tf, data_in, "data_in");
    sc_trace(tf, data_out_async, "data_out_async");
    sc_trace(tf, data_out_sync, "data_out_sync");

    // Initialization
    rst_async_n.write(true); // Deassert async reset (active low)
    rst_sync.write(false);   // Deassert sync reset (active high)
    data_in.write(10);

    std::cout << "@" << sc_time_stamp() << " Starting simulation..." << std::endl;
    sc_start(15, SC_NS);

    // 1. Trigger Asynchronous Reset (Mid-cycle)
    std::cout << "@" << sc_time_stamp() << " Asserting Async Reset (rst_async_n = 0)" << std::endl;
    rst_async_n.write(false);
    sc_start(10, SC_NS);
    std::cout << "@" << sc_time_stamp() << " Deasserting Async Reset" << std::endl;
    rst_async_n.write(true);
    sc_start(15, SC_NS);

    // 2. Trigger Synchronous Reset
    std::cout << "@" << sc_time_stamp() << " Asserting Sync Reset (rst_sync = 1)" << std::endl;
    rst_sync.write(true);
    sc_start(15, SC_NS);
    std::cout << "@" << sc_time_stamp() << " Deasserting Sync Reset" << std::endl;
    rst_sync.write(false);

    // 3. Normal Operation Change
    std::cout << "@" << sc_time_stamp() << " Changing data_in to 42" << std::endl;
    data_in.write(42);
    sc_start(30, SC_NS);

    std::cout << "@" << sc_time_stamp() << " Simulation complete." << std::endl;

    sc_close_vcd_trace_file(tf);
    return 0;
}
```

### Understanding the Simulation Output

When you run this code, the SystemC kernel enforces the semantics you declared using the `sc_unwind_exception`:
1. When `rst_async_n` drops to `false`, the `async_reset_thread` immediately aborts its current execution in the `while(true)` loop and jumps back to the very top of the function, driving `data_out_async` to `0`. It does not wait for `clk`.
2. When `rst_sync` jumps to `true`, the `sync_reset_thread` behaves similarly, *but* it waits until the next rising edge of `clk.pos()` before throwing the unwind exception and jumping back to the top of its function.

## HLS LRM Restrictions on Resets

When targeting physical silicon, the SystemC Synthesis Subset LRM enforces several strict rules regarding resets:

1. **Only Ports/Signals Allowed:** The argument passed to `async_reset_signal_is` must be an `sc_in<bool>` or an `sc_signal<bool>`. You cannot use a local boolean variable or a complex datatype.
2. **Single Reset:** A thread can generally only have one primary reset signal registered via these macros.
3. **No Variable Declarations with Initialization:** Do not initialize local variables in their declaration if they are intended to represent hardware state holding registers. Initialize them inside the reset block explicitly.

   *Incorrect:* `int count = 0;` (HLS tools often ignore C++ initialization).
   *Correct:* Declare `int count;` outside the loop, and write `count = 0;` before the first `wait()`.

By adhering strictly to these LRM guidelines, your C++ simulation will exactly match the RTL hardware generated by your HLS compiler.

## Lesson 135: Loop Unrolling, Pipelining, and Hardware Scheduling

Canonical lesson: https://www.learn-systemc.com/tutorials/129-loop-unrolling-pipelining-and-hardware-scheduling

A deep dive into loop unrolling, pipelining pragmas, and hardware scheduling in SystemC High-Level Synthesis (HLS).

## How to Read This Lesson

# Loop Unrolling and Pipelining in HLS

In standard C++ software, loops execute sequentially on a CPU. You don't have to worry about how long they take in terms of "clock cycles," only their general algorithmic complexity (O(N)).

In High-Level Synthesis (HLS), however, C++ loops are physically transformed into silicon. The way you write your loopâ€”and specifically where you place `wait()` statementsâ€”dictates whether the HLS tool generates massive parallel combinational logic, sequential state machines, or optimized hardware pipelines.

It is crucial to understand the difference between how the **Accellera SystemC Simulation Kernel** treats a loop and how an **HLS Compiler** treats it.

## Standard and source context

## The Kernel Reality vs. The HLS Compiler

When you compile a SystemC model with GCC or Clang and link against the Accellera kernel, your loop is just a standard C++ loop. It executes sequentially on your host machine's CPU. If there is no `wait()`, the loop runs to completion in a single delta cycle, blocking the cooperative scheduler (`sc_simcontext::crunch()`). If there is a `wait()`, the `sc_thread_process` saves its stack to a coroutine (QuickThreads/pthreads) and yields control back to the scheduler, to be resumed on the next clock edge.

An HLS compiler (like Siemens Catapult or Cadence Stratus) behaves very differently. It parses the Abstract Syntax Tree (AST) of your C++ code. It uses the `wait()` statements as explicit **register boundaries** to slice your C++ code into a Finite State Machine (FSM).

### 1. Loops Without `wait()`: Unrolling and Combinational Logic

If you write a `for` or `while` loop that does **not** contain a `wait()` statement, you are instructing the HLS compiler that all iterations of this loop must execute in the *same clock cycle*.

```cpp
// Executed entirely within one clock cycle
int sum = 0;
for(int i = 0; i < 4; i++) {
    sum += data[i];
}
result.write(sum);
wait(); // Clock edge occurs HERE
```

To achieve this physically, the HLS tool must **unroll** the loop. It flattens the AST, creating four separate adders in hardware and chaining them together as pure combinational logic.
*   **The Catch:** If your loop iterates 10,000 times, the tool will try to generate 10,000 adders in a massive combinational chain. This will fail physical timing constraints (the clock period). Therefore, loops without `wait()` must have a small, statically determinable number of iterations.

### 2. Loops With `wait()`: Sequential Execution

If you place a `wait()` inside the loop, the HLS tool slices the AST at that boundary, generating an FSM state transition.

```cpp
int sum = 0;
for(int i = 0; i < 4; i++) {
    sum += data[i];
    wait(); // Clock edge occurs on EVERY iteration
}
result.write(sum);
```

In this case, the HLS tool only needs to generate **one** physical adder. On clock cycle 1 (State 1), it adds `data[0]`. On cycle 2 (State 2), it adds `data[1]`. The loop will take exactly 4 clock cycles to complete, saving massive silicon area at the cost of latency.

## Tool-Specific Pragmas: Unrolling and Pipelining

Because SystemC is standard C++, it doesn't have native language keywords for hardware micro-architecture. EDA vendors provide compiler directives (`#pragma`) to control exactly how the AST is transformed.

*   **`#pragma HLS UNROLL`**: Tells the compiler to explicitly replicate the hardware logic for the loop body. You can specify a factor (e.g., `factor=2`) to partially unroll a loop, balancing area and speed.
*   **`#pragma HLS PIPELINE`**: Rather than waiting for the entire loop iteration to finish, pipelining creates shift registers in the datapath, starting the *next* iteration of the loop while the *current* iteration is still executing. The time between starting consecutive iterations is known as the **Initiation Interval (II)**.

## Synthesis Subset LRM Restrictions

When dealing with loops, the SystemC Synthesis Subset 1.4.7 mandates:
1.  **Static Bounds for Unrolling:** If a loop contains no `wait()` (meaning it must be completely unrolled into combinational logic), the number of iterations **must** be statically determinable at compile time. You cannot use a dynamically changing port value as the termination condition for a loop without a `wait()`.
2.  **No `wait()` in functions:** Generally, if a helper function contains a `wait()`, it must be inlined into the parent thread, and the parent thread's FSM scheduling is affected.

## End-to-End Example: A Dot Product Unit

Below is a complete, compilable SystemC model of a Dot Product unit. The loop inside `compute_thread` lacks a `wait()`, making it a prime candidate for Loop Unrolling by an HLS compiler.

```cpp
#include <systemc.h>

// -------------------------------------------------------------------------
// Synthesizable Hardware Module
// -------------------------------------------------------------------------
SC_MODULE(DotProductUnit) {
    sc_in<bool> clk;
    sc_in<bool> rst_n;

    // Arrays of ports for input vectors
    sc_in<int> a[4];
    sc_in<int> b[4];
    sc_in<bool> start;

    sc_out<int> result;
    sc_out<bool> valid;

    void compute_thread() {
        // --- RESET BLOCK ---
        result.write(0);
        valid.write(false);
        wait();

        // --- FUNCTIONAL BLOCK ---
        while (true) {
            if (start.read()) {
                int sum = 0;

                // --- LOOP UNROLLING CANDIDATE ---
                // Because there is no wait() inside this loop, the HLS compiler
                // will fully unroll this, generating 4 parallel multipliers
                // and an adder tree that executes in a single clock cycle.
                //
                // Example vendor directive:
                // #pragma HLS UNROLL
                for (int i = 0; i < 4; i++) {
                    sum += a[i].read() * b[i].read();
                }

                result.write(sum);
                valid.write(true);
            } else {
                valid.write(false);
            }
            wait(); // End of the clock cycle state
        }
    }

    SC_CTOR(DotProductUnit) {
        SC_CTHREAD(compute_thread, clk.pos());
        async_reset_signal_is(rst_n, false);
    }
};

// -------------------------------------------------------------------------
// Testbench / Simulation
// -------------------------------------------------------------------------
int sc_main(int argc, char* argv[]) {
    sc_clock clk("clk", 10, SC_NS);
    sc_signal<bool> rst_n;
    sc_signal<bool> start;

    sc_signal<int> a[4];
    sc_signal<int> b[4];
    sc_signal<int> result;
    sc_signal<bool> valid;

    // Instantiate and bind
    DotProductUnit dut("dut");
    dut.clk(clk);
    dut.rst_n(rst_n);
    dut.start(start);
    for(int i = 0; i < 4; ++i) {
        dut.a[i](a[i]);
        dut.b[i](b[i]);
    }
    dut.result(result);
    dut.valid(valid);

    // Initialization
    rst_n.write(false); // Assert reset
    start.write(false);
    for(int i = 0; i < 4; ++i) {
        a[i].write(0);
        b[i].write(0);
    }

    sc_start(15, SC_NS);
    rst_n.write(true); // Release reset

    // Test Case: Provide vector data
    std::cout << "@" << sc_time_stamp() << " Feeding inputs..." << std::endl;
    for(int i = 0; i < 4; ++i) {
        a[i].write(i + 1); // Vector A: [1, 2, 3, 4]
        b[i].write(2);     // Vector B: [2, 2, 2, 2]
    }
    start.write(true);

    // Step one clock cycle to capture inputs
    sc_start(10, SC_NS);
    start.write(false);

    // Step one more clock cycle to propagate outputs
    sc_start(10, SC_NS);

    // Expected: (1*2) + (2*2) + (3*2) + (4*2) = 2 + 4 + 6 + 8 = 20
    std::cout << "@" << sc_time_stamp() << " Result: " << result.read()
              << " (Expected 20)" << std::endl;
    std::cout << "Valid: " << (valid.read() ? "true" : "false") << std::endl;

    return 0;
}
```

By carefully managing loops and `wait()` statements according to the Synthesis Subset LRM, you retain absolute control over whether your SystemC algorithm is synthesized into parallel combinational hardware or a sequential FSM, even though the Accellera kernel executes them all identically as software.

## Lesson 136: HLS Synthesis Subset API and Portability Field Guide

Canonical lesson: https://www.learn-systemc.com/tutorials/136-hls-synthesis-subset-api-and-portability-field-guide

A SystemC synthesis subset guide for synthesizable processes, fixed-point types, resets, ports, and common non-synthesizable traps.

## How to Read This Lesson

SystemC for simulation and SystemC for synthesis are related, but not identical. Simulation asks, "does this C++ model behave correctly under the SystemC kernel?" Synthesis asks, "can a tool infer stable hardware from this subset?"

## Standard and source context

Use `Docs/LRMs/SystemC_Synthesis_Subset_1_4_7.pdf`. For implementation context, also keep the core datatype and process lessons nearby because synthesis tools still rely on the public SystemC API shape.

## Process Subset

`SC_METHOD`, `SC_THREAD`, and `SC_CTHREAD` are all legal SystemC concepts, but synthesis tools usually prefer clocked, statically analyzable structure. `SC_CTHREAD`, `wait`, `reset_signal_is`, and `async_reset_signal_is` are common in HLS-style examples because the clock boundary is visible.

Use static sensitivity for combinational `SC_METHOD` blocks. Use clocked threads or cthreads for state. Avoid dynamic process creation in synthesizable blocks.

## Ports and Hardware Boundaries

`sc_in`, `sc_out`, `sc_inout`, `sc_signal`, `sc_clock`, `sc_in_resolved`, `sc_out_resolved`, `sc_in_rv`, `sc_out_rv`, and related port names describe hardware boundaries.

For synthesis, ports are contracts. A port is not just a convenient way to pass a C++ value; it implies direction, timing, and possible hardware interface shape.

## Datatype Subset

`sc_uint`, `sc_int`, `sc_biguint`, `sc_bigint`, `sc_signed`, `sc_unsigned`, `sc_fixed`, `sc_ufixed`, `sc_fix`, `sc_ufix`, `sc_lv`, `sc_bv`, `sc_lv_base`, `sc_bit`, and `sc_logic` give you bit width, signedness, four-state logic, or fixed-point behavior.

The synthesis subset also talks about `sc_signals` as a family concept, not only one class name. Read it as the signal/channel boundary where hardware-like communication becomes visible to the tool.

The trap is performance and portability. A bit-accurate type is the right choice when the hardware contract needs bit accuracy. It is not automatically the right choice for high-level algorithm exploration.

## Bit Select, Range, and Concatenation Names

`sc_bitref_r`, `sc_concatref`, `sc_concref`, `sc_concref_r`, `sc_int_bitref`, `sc_int_bitref_r`, `sc_uint_bitref_r`, `sc_signed_bitref_r`, `sc_signed_subref`, `sc_signed_subref_r`, `sc_unsigned_subref`, and `sc_unsigned_subref_r` are the proxy names behind bit selects, ranges, and concatenations.

`sc_unsigned_bitref_r` is the read-side unsigned bit-reference variant. It is the sort of helper that appears when a synthesizable expression selects or forwards a single bit from a wider unsigned value.

These are useful because hardware code often manipulates slices. They are also a reminder that C++ syntax may expand into library proxy objects. Keep expressions simple when synthesis readability matters.

## Fixed-Point Policy Names

`sc_fxcast_switch`, `sc_fxnum_bitref`, `sc_fxnum_subref`, `sc_fixnum`, `sc_numrep`, and fixed-point quantization/overflow policy names belong to numeric control.

When reviewing HLS code, ask whether the numeric policy is an engineering decision or an accidental default. Bit width, quantization, saturation, and rounding should be written down.

## Trace and Non-Synthesis Names

`sc_close_wif_trace_file`, `sc_close_isdb_trace_file`, and tracing APIs are simulation/debug aids. They are useful in testbenches but should not be mistaken for synthesizable hardware.

Likewise, dynamic allocation, unbounded loops, host I/O, file access, unrestricted pointers, and data-dependent recursion are usually outside a practical synthesis subset.

## Senior Review Checklist

- Is every hardware state update clocked?
- Are resets explicit and tool-supported?
- Are loops bounded or intentionally pipelined?
- Are bit widths and fixed-point policies documented?
- Is TLM isolated from synthesizable datapath code?

## Lesson 137: Beginner Pitfalls & FAQ

Canonical lesson: https://www.learn-systemc.com/tutorials/130-beginner-pitfalls-and-faq

Addressing the most common SystemC gotchas: SC_METHOD vs SC_THREAD, delta cycle confusion, and simulation hangs.

## How to Read This Lesson

# Common Beginner Pitfalls & FAQs

When learning SystemC, moving from standard C++ sequential execution to a concurrent, event-driven hardware simulation paradigm can be jarring. This guide answers the most critical, highly-technical beginner FAQs, providing complete, runnable code examples that adhere to the IEEE 1666 standard, and delving into the **Accellera SystemC kernel source code** to explain *why* these issues occur.

## Standard and source context

## 1. SC_METHOD vs SC_THREAD: The `wait()` Crash

**The Problem:** You wrote a simple module, called `wait()`, and your simulation crashes with a runtime error: `Error: (E519) wait() is only allowed in SC_THREADs and SC_CTHREADs`.

**The Technical Reality (IEEE 1666 & Accellera Kernel):**
SystemC uses **co-operative multitasking** managed by its discrete-event scheduler (`sc_simcontext`).
- An `SC_METHOD` is modeled under the hood by the `sc_method_process` class. It executes as a standard C++ function call via `sc_method_handle->semantics()`. Once the scheduler invokes it, it **must** run to completion and return control. It has no dedicated stack. When you call `wait()` inside an `SC_METHOD`, the `sc_set_curr_simcontext` checks `sc_get_curr_process_handle()->process_kind()`. Because it's an `SC_METHOD_PROC_`, it throws the `E519` error because there is no coroutine state (like `sc_coroutine` or `QuickThreads`/`ucontext`) to save.
- An `SC_THREAD` uses the `sc_thread_process` class, which manages a **coroutine** (fiber/user-level thread). The kernel allocates a dedicated stack (e.g., via `qt_allocate` or `makecontext`). When you call `wait()`, `sc_thread_process::suspend_me()` is invoked, saving the current CPU registers to the stack context, and yielding control back to the central `sc_simcontext::crunch()` loop.

**The Fix:**
Use `SC_THREAD` for sequential logic requiring suspension over time. Use `SC_METHOD` for purely combinatorial logic.

```cpp
#include <systemc>

SC_MODULE(MethodThreadExample) {
    sc_core::sc_in<bool> clk;

    SC_CTOR(MethodThreadExample) {
        // SC_METHOD cannot wait. It runs when clk changes.
        SC_METHOD(combinatorial_logic);
        sensitive << clk;

        // SC_THREAD can wait.
        SC_THREAD(sequential_logic);
        sensitive << clk.pos();
    }

    void combinatorial_logic() {
        // NO wait() here! Runs to completion.
        std::cout << "@" << sc_core::sc_time_stamp()
                  << ": Evaluated combinatorial_logic" << std::endl;
    }

    void sequential_logic() {
        while(true) {
            wait(); // Suspends execution until the next positive clock edge
            std::cout << "@" << sc_core::sc_time_stamp()
                      << ": Evaluated sequential_logic on clock edge" << std::endl;
        }
    }
};

int sc_main(int argc, char* argv[]) {
    sc_core::sc_clock clock("clock", 10, sc_core::SC_NS);
    MethodThreadExample example("example");
    example.clk(clock);

    sc_core::sc_start(30, sc_core::SC_NS);
    return 0;
}
```

## 2. Why Doesn't My Signal Update Immediately? (The Delta Cycle)

**The Problem:** You write a value to a signal and immediately read it on the next line, but it still holds the old value.

**The Technical Reality (IEEE 1666 & Accellera Kernel):**
This is the core of the **Evaluate-Update paradigm**. In hardware, parallel registers update simultaneously.
In the Accellera kernel, `sc_signal<T>` inherits from `sc_prim_channel`. When you call `my_signal.write(new_val)`, the `sc_signal::write()` method essentially does:
1. Compares `new_val` with `m_new_val`.
2. If they differ, it sets `m_new_val = new_val` and calls `request_update()`.
3. `request_update()` pushes the `sc_prim_channel` into `sc_simcontext::m_update_list`.

The immediate `read()` call still returns `m_cur_val`. Only when all processes finish evaluating, the scheduler transitions to the **Update Phase**. It iterates over `m_update_list`, calling the virtual `update()` method on each channel. `sc_signal::update()` executes `m_cur_val = m_new_val;`, making the new value visible and notifying events (`m_value_changed_event.notify()`).

**The Fix:**
Wait for the delta cycle to progress, or use standard C++ variables for immediate updates.

```cpp
#include <systemc>

SC_MODULE(DeltaCycleDemo) {
    sc_core::sc_signal<bool> my_signal;

    SC_CTOR(DeltaCycleDemo) {
        SC_THREAD(demo_thread);
    }

    void demo_thread() {
        my_signal.write(true);

        // This read returns the OLD value (false) because the update phase hasn't occurred.
        std::cout << "Before delta delay, my_signal = " << my_signal.read() << std::endl;

        // Advance simulation by one delta cycle (SC_ZERO_TIME)
        wait(sc_core::SC_ZERO_TIME);

        // Now it's true!
        std::cout << "After delta delay, my_signal = " << my_signal.read() << std::endl;
    }
};

int sc_main(int argc, char* argv[]) {
    DeltaCycleDemo demo("demo");
    sc_core::sc_start();
    return 0;
}
```

## 3. Simulation Hangs at Time 0

**The Problem:** Simulation time never advances. `sc_time_stamp()` is stuck at `0 s`, freezing the kernel.

**The Technical Reality (Accellera Kernel):**
You have an **infinite delta-cycle loop**.
In `sc_simcontext::crunch()`, the kernel loops over the evaluate and update phases:
```cpp
while( true ) {
    // Evaluate Phase: Run all runnable processes
    // ...
    // Update Phase: Update channels in m_update_list
    // ...
    // Check for delta events. If events are at current time, repeat loop.
}
```
If Process A writes a signal triggering Process B, and Process B writes a signal triggering Process A, `m_delta_count` increments endlessly while the scheduler is locked in the `crunch()` loop. Because no events are scheduled for *future* time (`sc_time > 0`), the time advancement logic is never reached.

**The Fix:**
Break combinatorial loops by inserting clocked delays (`wait(sc_time)` or `wait()` with a clock) to schedule events into the `m_timed_events` priority queue, allowing time to advance properly.

```cpp
#include <systemc>

SC_MODULE(DeltaLoopFix) {
    sc_core::sc_signal<bool> sig_a;
    sc_core::sc_signal<bool> sig_b;

    SC_CTOR(DeltaLoopFix) {
        SC_METHOD(process_a);
        sensitive << sig_b;

        SC_THREAD(process_b); // Changed to SC_THREAD to break the loop over time
        sensitive << sig_a;
    }

    void process_a() {
        // Combinatorial assignment
        sig_a.write(!sig_b.read());
    }

    void process_b() {
        while(true) {
            // Wait for time to advance, scheduling this process into the future
            wait(10, sc_core::SC_NS);
            sig_b.write(!sig_a.read());
            std::cout << "Time: " << sc_core::sc_time_stamp() << std::endl;
        }
    }
};

int sc_main(int argc, char* argv[]) {
    DeltaLoopFix fix("fix");
    // Without the wait(10, SC_NS) in process_b, sc_start() would hang forever.
    sc_core::sc_start(50, sc_core::SC_NS);
    return 0;
}
```

## 4. `next_trigger()` vs `sensitive <<`

**The Technical Reality (Accellera Kernel):**
Static sensitivity (`sensitive <<`) is bound during elaboration. The `sc_process_b` class stores these static events in a vector. Dynamic sensitivity (`next_trigger()`) temporaily overrides this by pushing a new event into `m_trigger_event` or setting `m_timeout_event` inside the kernel. Once the `SC_METHOD` executes again, the dynamic sensitivity is cleared, and it reverts to the static list unless `next_trigger()` is called again. This provides immense flexibility without complex state-machine checks.
