Skip to main content

bitloom_sim/
systemc_tlm.rs

1//! SystemC TLM-2.0 LT product path (FR101 / Epic 46 / Story 46.2).
2//!
3//! Emits includeable/linkable C++ using IEEE 1666 TLM-2.0 loosely-timed
4//! `b_transport` + `tlm_generic_payload`. **Not** host Rust FL (FR47).
5//! Does **not** replace cycle-accurate `Sim::tick` (AD-5) and does **not**
6//! claim default TLM≡CA (FR100).
7
8use std::fs;
9use std::io;
10use std::path::{Path, PathBuf};
11use std::process::Command;
12
13use bitloom_hir::{FrozenHir, GroundType, PortDirection};
14
15/// Pinned Accellera SystemC line documented for FR101 MVP (Debian/pkg-config).
16pub const SYSTEMC_PIN_VERSION: &str = "2.3.3";
17
18/// Resolved SystemC toolchain (pkg-config).
19#[derive(Debug, Clone)]
20pub struct SystemcToolchain {
21    pub version: String,
22    pub cflags: String,
23    pub libs: String,
24}
25
26/// Emit a SystemC TLM-2.0 **LT-only** fixture under `out_dir` from `FrozenHir`.
27///
28/// Writes:
29/// - `bitloom_tlm_lt.hpp` / `bitloom_tlm_lt.cpp` — target + initiator (D1)
30/// - `sc_main.cpp` — runnable smoke (D3)
31/// - `Makefile` — links via `pkg-config systemc` (D4)
32///
33/// Abstraction: **LT-only** (`b_transport`). AT (`nb_transport_*`) is out of MVP.
34pub fn emit_systemc_tlm_lt(hir: &FrozenHir, out_dir: &Path) -> io::Result<PathBuf> {
35    fs::create_dir_all(out_dir)?;
36    let top =
37        hir.circuit().modules.first().ok_or_else(|| {
38            io::Error::new(io::ErrorKind::InvalidData, "FrozenHir has no modules")
39        })?;
40    let mod_name = sanitize_cpp_ident(&top.name);
41    let regs = collect_data_ports(top);
42    if regs.is_empty() {
43        return Err(io::Error::new(
44            io::ErrorKind::InvalidData,
45            "no non-clock/reset data ports to map into TLM LT register bank",
46        ));
47    }
48
49    fs::write(
50        out_dir.join("bitloom_tlm_lt.hpp"),
51        render_header(&mod_name, &regs),
52    )?;
53    fs::write(
54        out_dir.join("bitloom_tlm_lt.cpp"),
55        render_impl(&mod_name, &regs),
56    )?;
57    fs::write(out_dir.join("sc_main.cpp"), render_sc_main(&mod_name))?;
58    fs::write(out_dir.join("Makefile"), render_makefile())?;
59    fs::write(out_dir.join("README.md"), render_fixture_readme(&mod_name))?;
60    Ok(out_dir.to_path_buf())
61}
62
63/// Alias for product naming symmetry with `emit_functional_crate`.
64pub fn generate_systemc_tlm_lt(hir: &FrozenHir, out_dir: &Path) -> io::Result<PathBuf> {
65    emit_systemc_tlm_lt(hir, out_dir)
66}
67
68/// Resolve SystemC via `pkg-config`. Missing install → readable `Err` (never silent OK).
69pub fn resolve_systemc() -> Result<SystemcToolchain, String> {
70    let version = pkg_config_stdout(&["--modversion", "systemc"]).map_err(|e| {
71        format!(
72            "SystemC not found ({e}). Install libsystemc-dev (pinned {SYSTEMC_PIN_VERSION}) \
73             or ensure `pkg-config --exists systemc`. See docs/fr101-systemc-tlm.md."
74        )
75    })?;
76    let cflags = pkg_config_stdout(&["--cflags", "systemc"])
77        .map_err(|e| format!("pkg-config --cflags systemc failed: {e}"))?;
78    let libs = pkg_config_stdout(&["--libs", "systemc"])
79        .map_err(|e| format!("pkg-config --libs systemc failed: {e}"))?;
80    Ok(SystemcToolchain {
81        version: version.trim().to_string(),
82        cflags: cflags.trim().to_string(),
83        libs: libs.trim().to_string(),
84    })
85}
86
87/// Build and run the emitted LT smoke (`make run` in `out_dir`).
88///
89/// Requires SystemC; failures include install/version hints (D4).
90pub fn build_and_run_tlm_lt_smoke(out_dir: &Path) -> Result<String, String> {
91    let tc = resolve_systemc()?;
92    if !tc.version.starts_with(SYSTEMC_PIN_VERSION)
93        && !tc.version.starts_with("2.3.")
94        && std::env::var_os("BITLOOM_SYSTEMC_ALLOW_ANY").is_none()
95    {
96        // Soft warn path: still try build, but surface pin in message on failure.
97        let _ = &tc;
98    }
99    let makefile = out_dir.join("Makefile");
100    if !makefile.is_file() {
101        return Err(format!(
102            "missing Makefile under {}; run emit_systemc_tlm_lt first",
103            out_dir.display()
104        ));
105    }
106    let status = Command::new("make")
107        .arg("run")
108        .current_dir(out_dir)
109        .output()
110        .map_err(|e| format!("spawn make: {e}"))?;
111    let stdout = String::from_utf8_lossy(&status.stdout).into_owned();
112    let stderr = String::from_utf8_lossy(&status.stderr).into_owned();
113    if !status.status.success() {
114        return Err(format!(
115            "SystemC TLM LT smoke failed (SystemC {}, pin {}).\nstdout:\n{stdout}\nstderr:\n{stderr}\n\
116             Install: apt install libsystemc-dev (or equivalent). See docs/fr101-systemc-tlm.md.",
117            tc.version, SYSTEMC_PIN_VERSION
118        ));
119    }
120    if !stdout.contains("BITLOOM_TLM_LT_OK") {
121        return Err(format!(
122            "smoke ran but missing BITLOOM_TLM_LT_OK marker.\nstdout:\n{stdout}\nstderr:\n{stderr}"
123        ));
124    }
125    Ok(stdout)
126}
127
128struct MappedPort {
129    name: String,
130    width: u32,
131    dir: PortDirection,
132    offset: u32,
133}
134
135fn collect_data_ports(m: &bitloom_hir::Module) -> Vec<MappedPort> {
136    let mut out = Vec::new();
137    let mut offset = 0u32;
138    for p in &m.ports {
139        match p.ty {
140            GroundType::Clock | GroundType::Reset => continue,
141            GroundType::UInt { width } | GroundType::SInt { width } => {
142                out.push(MappedPort {
143                    name: p.name.clone(),
144                    width,
145                    dir: p.direction,
146                    offset,
147                });
148                offset = offset.saturating_add(8); // 8-byte aligned slots
149            }
150            GroundType::Bool => {
151                out.push(MappedPort {
152                    name: p.name.clone(),
153                    width: 1,
154                    dir: p.direction,
155                    offset,
156                });
157                offset = offset.saturating_add(8);
158            }
159            GroundType::Analog => {}
160        }
161    }
162    out
163}
164
165fn sanitize_cpp_ident(s: &str) -> String {
166    let mut out: String = s
167        .chars()
168        .map(|c| {
169            if c.is_ascii_alphanumeric() || c == '_' {
170                c
171            } else {
172                '_'
173            }
174        })
175        .collect();
176    if out.is_empty() || out.chars().next().is_some_and(|c| c.is_ascii_digit()) {
177        out = format!("M_{out}");
178    }
179    out
180}
181
182fn render_header(mod_name: &str, regs: &[MappedPort]) -> String {
183    let mut decls = String::new();
184    for r in regs {
185        decls.push_str(&format!(
186            "  // port {} width={} dir={:?} @0x{:x}\n",
187            r.name, r.width, r.dir, r.offset
188        ));
189    }
190    format!(
191        r#"#pragma once
192// Generated by Bitloom emit_systemc_tlm_lt (FR101).
193// LT-only TLM-2.0 (b_transport + tlm_generic_payload). Not Rust FL (FR47).
194// Product: Bitloom. Unrelated to samitbasu/rhdl.
195// AD-5 (revised): SystemC TLM-2.0 product path allowed; does not replace Sim::tick.
196
197#include <systemc>
198#include <tlm>
199#include <tlm_utils/simple_initiator_socket.h>
200#include <tlm_utils/simple_target_socket.h>
201#include <cstdint>
202#include <cstring>
203#include <vector>
204
205namespace bitloom_tlm {{
206
207struct {mod_name}LtTarget : sc_core::sc_module {{
208  tlm_utils::simple_target_socket<{mod_name}LtTarget> socket;
209  SC_HAS_PROCESS({mod_name}LtTarget);
210  explicit {mod_name}LtTarget(sc_core::sc_module_name name);
211  void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay);
212
213private:
214  std::vector<std::uint8_t> mem_;
215}};
216
217struct {mod_name}LtInitiator : sc_core::sc_module {{
218  tlm_utils::simple_initiator_socket<{mod_name}LtInitiator> socket;
219  SC_HAS_PROCESS({mod_name}LtInitiator);
220  explicit {mod_name}LtInitiator(sc_core::sc_module_name name);
221  void run();
222}};
223
224{decls}
225}}  // namespace bitloom_tlm
226"#,
227        mod_name = mod_name,
228        decls = decls,
229    )
230}
231
232fn render_impl(mod_name: &str, regs: &[MappedPort]) -> String {
233    let mem_size = regs
234        .last()
235        .map(|r| r.offset.saturating_add(8))
236        .unwrap_or(8)
237        .max(16);
238    format!(
239        r#"#include "bitloom_tlm_lt.hpp"
240#include <iostream>
241
242namespace bitloom_tlm {{
243
244{mod_name}LtTarget::{mod_name}LtTarget(sc_core::sc_module_name name)
245    : sc_module(name), socket("socket"), mem_({mem_size}, 0) {{
246  socket.register_b_transport(this, &{mod_name}LtTarget::b_transport);
247}}
248
249void {mod_name}LtTarget::b_transport(tlm::tlm_generic_payload& trans,
250                                     sc_core::sc_time& delay) {{
251  (void)delay;
252  auto cmd = trans.get_command();
253  auto addr = static_cast<std::uint64_t>(trans.get_address());
254  auto* ptr = trans.get_data_ptr();
255  auto len = trans.get_data_length();
256  if (addr + len > mem_.size()) {{
257    trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
258    return;
259  }}
260  if (cmd == tlm::TLM_WRITE_COMMAND) {{
261    std::memcpy(mem_.data() + addr, ptr, len);
262  }} else if (cmd == tlm::TLM_READ_COMMAND) {{
263    std::memcpy(ptr, mem_.data() + addr, len);
264  }} else {{
265    trans.set_response_status(tlm::TLM_COMMAND_ERROR_RESPONSE);
266    return;
267  }}
268  trans.set_response_status(tlm::TLM_OK_RESPONSE);
269}}
270
271{mod_name}LtInitiator::{mod_name}LtInitiator(sc_core::sc_module_name name)
272    : sc_module(name), socket("socket") {{
273  SC_THREAD(run);
274}}
275
276void {mod_name}LtInitiator::run() {{
277  unsigned char buf[4] = {{0x11, 0x22, 0x33, 0x44}};
278  tlm::tlm_generic_payload trans;
279  sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
280  trans.set_command(tlm::TLM_WRITE_COMMAND);
281  trans.set_address(0);
282  trans.set_data_ptr(buf);
283  trans.set_data_length(4);
284  trans.set_streaming_width(4);
285  trans.set_byte_enable_ptr(nullptr);
286  trans.set_dmi_allowed(false);
287  trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);
288  socket->b_transport(trans, delay);
289  if (!trans.is_response_ok()) {{
290    std::cerr << "BITLOOM_TLM_LT_FAIL write\n";
291    sc_core::sc_stop();
292    return;
293  }}
294  unsigned char rbuf[4] = {{0, 0, 0, 0}};
295  trans.set_command(tlm::TLM_READ_COMMAND);
296  trans.set_data_ptr(rbuf);
297  trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);
298  socket->b_transport(trans, delay);
299  if (!trans.is_response_ok() || std::memcmp(buf, rbuf, 4) != 0) {{
300    std::cerr << "BITLOOM_TLM_LT_FAIL readback\n";
301    sc_core::sc_stop();
302    return;
303  }}
304  std::cout << "BITLOOM_TLM_LT_OK\n";
305  sc_core::sc_stop();
306}}
307
308}}  // namespace bitloom_tlm
309"#,
310        mod_name = mod_name,
311        mem_size = mem_size,
312    )
313}
314
315fn render_sc_main(mod_name: &str) -> String {
316    format!(
317        r#"#include "bitloom_tlm_lt.hpp"
318
319int sc_main(int, char*[]) {{
320  bitloom_tlm::{mod_name}LtInitiator init("init");
321  bitloom_tlm::{mod_name}LtTarget tgt("tgt");
322  init.socket.bind(tgt.socket);
323  sc_core::sc_start();
324  return 0;
325}}
326"#,
327        mod_name = mod_name
328    )
329}
330
331fn render_makefile() -> String {
332    format!(
333        r#"# Bitloom FR101 SystemC TLM-2.0 LT smoke (pin {pin}).
334# Requires: pkg-config systemc + g++ (libsystemc-dev).
335CXX ?= g++
336CXXFLAGS ?= -std=c++17 -Wall -O0 $(shell pkg-config --cflags systemc)
337LDFLAGS ?= $(shell pkg-config --libs systemc) -pthread
338
339.PHONY: all run clean
340
341all: bitloom_tlm_lt_smoke
342
343bitloom_tlm_lt_smoke: sc_main.cpp bitloom_tlm_lt.cpp bitloom_tlm_lt.hpp
344	@if ! pkg-config --exists systemc; then \
345	  echo "error: SystemC not found. Install libsystemc-dev (pinned {pin}). See docs/fr101-systemc-tlm.md." >&2; \
346	  exit 1; \
347	fi
348	$(CXX) $(CXXFLAGS) sc_main.cpp bitloom_tlm_lt.cpp -o $@ $(LDFLAGS)
349
350run: bitloom_tlm_lt_smoke
351	./bitloom_tlm_lt_smoke
352
353clean:
354	rm -f bitloom_tlm_lt_smoke
355"#,
356        pin = SYSTEMC_PIN_VERSION
357    )
358}
359
360fn render_fixture_readme(mod_name: &str) -> String {
361    format!(
362        r#"# Bitloom SystemC TLM-2.0 LT fixture ({mod_name})
363
364Generated by `bitloom_sim::emit_systemc_tlm_lt` / `cargo bitloom gen-tlm` (**FR101**).
365
366- Abstraction: **LT-only** (`b_transport` + `tlm_generic_payload`)
367- Pin: SystemC **{pin}** (`pkg-config systemc`)
368- Not host Rust functional sim (**FR47**)
369- Cross-link: revised **AD-5** (ARCHITECTURE-SPINE); docs/fr101-systemc-tlm.md
370
371```bash
372make run
373# expects: BITLOOM_TLM_LT_OK
374```
375"#,
376        mod_name = mod_name,
377        pin = SYSTEMC_PIN_VERSION
378    )
379}
380
381fn pkg_config_stdout(args: &[&str]) -> Result<String, String> {
382    let out = Command::new("pkg-config")
383        .args(args)
384        .output()
385        .map_err(|e| format!("spawn pkg-config: {e}"))?;
386    if !out.status.success() {
387        return Err(String::from_utf8_lossy(&out.stderr).trim().to_string());
388    }
389    Ok(String::from_utf8_lossy(&out.stdout).into_owned())
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use bitloom_builder::{ElaborateSession, GroundType, Span};
396
397    fn tiny_hir() -> FrozenHir {
398        let mut s = ElaborateSession::new("TlmTiny");
399        s.begin_module("TlmTiny", Span::default());
400        s.add_input("clk", GroundType::Clock, Span::default());
401        s.add_input("rst", GroundType::Reset, Span::default());
402        s.add_output("data", GroundType::UInt { width: 8 }, Span::default());
403        s.begin_combinational(Span::default());
404        s.assign_lit("data", 0, Span::default());
405        s.end_process();
406        s.end_module();
407        s.finish().expect("TlmTiny")
408    }
409
410    #[test]
411    fn emit_writes_lt_sources() {
412        let hir = tiny_hir();
413        let dir = std::env::temp_dir().join(format!("bitloom-tlm-{}", std::process::id()));
414        let _ = fs::remove_dir_all(&dir);
415        let out = emit_systemc_tlm_lt(&hir, &dir).unwrap();
416        let hpp = fs::read_to_string(out.join("bitloom_tlm_lt.hpp")).unwrap();
417        assert!(hpp.contains("tlm_generic_payload") || hpp.contains("b_transport"));
418        assert!(hpp.contains("b_transport"));
419        assert!(hpp.contains("Bitloom"));
420        assert!(!hpp.to_lowercase().contains("functional_model"));
421        let mk = fs::read_to_string(out.join("Makefile")).unwrap();
422        assert!(mk.contains("systemc") && mk.contains(SYSTEMC_PIN_VERSION));
423    }
424}