1use std::fs;
9use std::io;
10use std::path::{Path, PathBuf};
11use std::process::Command;
12
13use bitloom_hir::{FrozenHir, GroundType, PortDirection};
14
15pub const SYSTEMC_PIN_VERSION: &str = "2.3.3";
17
18#[derive(Debug, Clone)]
20pub struct SystemcToolchain {
21 pub version: String,
22 pub cflags: String,
23 pub libs: String,
24}
25
26pub 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, ®s),
52 )?;
53 fs::write(
54 out_dir.join("bitloom_tlm_lt.cpp"),
55 render_impl(&mod_name, ®s),
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
63pub fn generate_systemc_tlm_lt(hir: &FrozenHir, out_dir: &Path) -> io::Result<PathBuf> {
65 emit_systemc_tlm_lt(hir, out_dir)
66}
67
68pub 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
87pub 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 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); }
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}