added verilog support

This commit is contained in:
ParkerTenBroeck 2026-03-13 13:28:26 -04:00
parent 5746846896
commit c3a3e89082
20 changed files with 633 additions and 88 deletions

View file

@ -9,7 +9,20 @@ use tokio::process::{Child, Command};
use crate::HResult;
const EMBEDDED_VHDL_UI_LIB: &[u8] = include_bytes!(env!("EMBEDDED_VHDL_CONN_LIB_PATH"));
const EMBEDDED_TB_VHDL: &str = include_str!("../../rtl/tb.vhdl");
const EMBEDDED_TB_VHDL: &str = include_str!("../shim/shim.vhdl");
const EMBEDDED_TB_VERILATOR: &str = include_str!("../shim/verilog.c");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Simulator {
Ghdl,
Verilator,
}
#[derive(Debug, Clone)]
pub struct BuildArtifact {
pub simulator: Simulator,
pub run_target: PathBuf,
}
async fn ensure_ok(child: Child) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let result = child.wait_with_output().await?;
@ -23,6 +36,137 @@ async fn ensure_ok(child: Child) -> Result<(), Box<dyn std::error::Error + Send
Ok(())
}
fn source_files(src: &Path) -> HResult<Vec<PathBuf>> {
let mut files = Vec::new();
for file in src.read_dir()?.flatten() {
let path = file.path();
if path.is_file() {
files.push(path.canonicalize()?);
}
}
files.sort();
Ok(files)
}
fn detect_simulator(files: &[PathBuf]) -> HResult<Simulator> {
let mut has_vhdl = false;
let mut has_verilog = false;
for file in files {
match file.extension().and_then(OsStr::to_str) {
Some("vhdl" | "vhd") => has_vhdl = true,
Some("v" | "sv") => has_verilog = true,
_ => {}
}
}
match (has_vhdl, has_verilog) {
(true, false) => Ok(Simulator::Ghdl),
(false, true) => Ok(Simulator::Verilator),
(true, true) => Err("mixed VHDL and Verilog sources are not supported yet".into()),
(false, false) => Err("no VHDL or Verilog source files found".into()),
}
}
async fn build_with_ghdl(
build: &Path,
files: &[PathBuf],
embedded_lib_path: &Path,
) -> HResult<BuildArtifact> {
let embedded_tb_path = build.join("tb.vhdl");
std::fs::write(&embedded_tb_path, EMBEDDED_TB_VHDL)?;
let mut cmd = Command::new("ghdl");
cmd.kill_on_drop(true);
cmd.args(["-i", "-g", "--std=08"]);
for file in files {
if matches!(
file.extension().and_then(OsStr::to_str),
Some("vhdl" | "vhd")
) {
cmd.arg(file);
}
}
cmd.arg(&embedded_tb_path.canonicalize()?);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
cmd.current_dir(build);
ensure_ok(cmd.spawn()?).await?;
let mut cmd = Command::new("ghdl");
cmd.kill_on_drop(true);
cmd.args(["-m", "--std=08"]);
cmd.arg(format!(
"-Wl,{}",
embedded_lib_path.canonicalize()?.display()
));
cmd.arg("tb");
cmd.current_dir(build);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
ensure_ok(cmd.spawn()?).await?;
Ok(BuildArtifact {
simulator: Simulator::Ghdl,
run_target: build.join("tb"),
})
}
async fn build_with_verilator(
build: &Path,
files: &[PathBuf],
embedded_lib_path: &Path,
) -> HResult<BuildArtifact> {
let embedded_tb_path = build.join("tb.cpp");
let obj_dir = build.join("obj_dir");
std::fs::write(&embedded_tb_path, EMBEDDED_TB_VERILATOR)?;
std::fs::create_dir_all(&obj_dir)?;
let mut cmd = Command::new("verilator");
cmd.kill_on_drop(true);
cmd.args(["--cc", "--exe", "--top-module", "circuit", "--Mdir"]);
cmd.arg(&obj_dir);
cmd.args(["-o", "tb"]);
cmd.args([
"-LDFLAGS",
&embedded_lib_path.canonicalize()?.display().to_string(),
]);
cmd.arg(&embedded_tb_path);
for file in files {
if matches!(file.extension().and_then(OsStr::to_str), Some("v" | "sv")) {
cmd.arg(file);
}
}
cmd.current_dir(build);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
ensure_ok(cmd.spawn()?).await?;
let mut cmd = Command::new("make");
cmd.kill_on_drop(true);
cmd.args(["-C"]);
cmd.arg(&obj_dir);
cmd.args(["-f", "Vcircuit.mk", "-j", "1"]);
cmd.current_dir(build);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
ensure_ok(cmd.spawn()?).await?;
Ok(BuildArtifact {
simulator: Simulator::Verilator,
run_target: obj_dir.join("tb"),
})
}
pub struct TempDir(PathBuf);
impl Drop for TempDir {
fn drop(&mut self) {
@ -49,7 +193,12 @@ impl AsRef<Path> for TempDir {
}
}
pub async fn copy_and_build(files: HashMap<String, String>) -> HResult<TempDir> {
pub struct TempBuild {
pub dir: TempDir,
pub artifact: BuildArtifact,
}
pub async fn copy_and_build(files: HashMap<String, String>) -> HResult<TempBuild> {
use std::hash::*;
let mut hasher = std::hash::DefaultHasher::default();
for (key, value) in &files {
@ -59,7 +208,7 @@ pub async fn copy_and_build(files: HashMap<String, String>) -> HResult<TempDir>
let hash = hasher.finish();
let mut work_dir = std::env::temp_dir();
work_dir.push(format!("ghdl-relay-{hash:x?}"));
work_dir.push(format!("hdl-relay-{hash:x?}"));
std::fs::create_dir_all(&work_dir)?;
let work_dir = TempDir(work_dir);
@ -69,49 +218,23 @@ pub async fn copy_and_build(files: HashMap<String, String>) -> HResult<TempDir>
std::fs::write(path, contents)?;
}
build(&work_dir, &work_dir).await?;
let artifact = build(&work_dir, &work_dir).await?;
Ok(work_dir)
Ok(TempBuild {
dir: work_dir,
artifact,
})
}
pub async fn build(build: &Path, src: &Path) -> HResult<()> {
std::fs::create_dir_all(build)?;
pub async fn build(build: &Path, src: &Path) -> HResult<BuildArtifact> {
let build = build.canonicalize()?;
let src = src.canonicalize()?;
std::fs::create_dir_all(&build)?;
let embedded_lib_path = build.join("libvhdl_conn.a");
let embedded_tb_path = build.join("tb.vhdl");
std::fs::write(&embedded_lib_path, EMBEDDED_VHDL_UI_LIB)?;
std::fs::write(&embedded_tb_path, EMBEDDED_TB_VHDL)?;
let mut cmd = Command::new("ghdl");
cmd.kill_on_drop(true);
cmd.args(["-i", "-g", "--std=08"]);
for file in src.read_dir().unwrap().flatten() {
if Path::new(&file.file_name()).extension() == Some(OsStr::new("vhdl")) {
cmd.arg(file.path().canonicalize()?);
}
let files = source_files(&src)?;
match detect_simulator(&files)? {
Simulator::Ghdl => build_with_ghdl(&build, &files, &embedded_lib_path).await,
Simulator::Verilator => build_with_verilator(&build, &files, &embedded_lib_path).await,
}
cmd.arg(&embedded_tb_path.canonicalize()?);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
cmd.current_dir(build);
ensure_ok(cmd.spawn()?).await?;
let mut cmd = Command::new("ghdl");
cmd.kill_on_drop(true);
cmd.args(["-m", "--std=08"]);
cmd.arg(format!(
"-Wl,{}",
embedded_lib_path.canonicalize()?.display()
));
cmd.arg("tb");
cmd.current_dir(build);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
ensure_ok(cmd.spawn()?).await?;
Ok(())
}

View file

@ -7,8 +7,7 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use std::{
net::{IpAddr, SocketAddr},
time::Duration,
net::{IpAddr, SocketAddr}, path::PathBuf, time::Duration
};
pub mod build;
@ -45,12 +44,13 @@ async fn not_found() -> impl IntoResponse {
(StatusCode::NOT_FOUND, "not found")
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
struct Config {
ip: IpAddr,
port: u16,
update_ms: u64,
workspace_ws: bool,
workspace_src: PathBuf,
}
impl Default for Config {
@ -59,7 +59,8 @@ impl Default for Config {
ip: IpAddr::from([127, 0, 0, 1]),
port: 8080,
update_ms: 30,
workspace_ws: true,
workspace_ws: false,
workspace_src: "./src".into(),
}
}
}
@ -92,6 +93,10 @@ fn parse_config_from_args() -> Result<Config, String> {
"--workspace" => {
cfg.workspace_ws = true;
}
"--workspace-src" => {
cfg.workspace_ws = true;
cfg.workspace_src = args.next().ok_or("missing value for --workspace-src")?.into();
}
"--help" | "-h" => {
return Err(
"usage: relay [--ip <ip>] [--port <port>] [--update-ms <ms>] [--workspace]"
@ -144,8 +149,9 @@ async fn main() {
"/ws/workspace",
get(move |ws: WebSocketUpgrade| {
let update_interval = update_interval;
let workspace_src = cfg.workspace_src;
async move {
ws.on_upgrade(move |socket| workspace::ws_handler(socket, update_interval))
ws.on_upgrade(move |socket| workspace::ws_handler(socket, workspace_src.clone(), update_interval))
}
}),
);

View file

@ -2,6 +2,8 @@ use std::path::Path;
use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
use crate::build::{BuildArtifact, Simulator};
pub struct Process {
pub child: Child,
pub stdin: ChildStdin,
@ -9,16 +11,26 @@ pub struct Process {
pub stderr: ChildStderr,
}
pub async fn run(artifact_dir: &Path) -> Result<Process, Box<dyn std::error::Error + Send + Sync>> {
let mut cmd = Command::new("ghdl");
cmd.args([
"-r",
"--std=08",
"tb",
"--stop-delta=4294967296",
"--unbuffered",
"--",
]);
pub async fn run(
artifact_dir: &Path,
artifact: &BuildArtifact,
) -> Result<Process, Box<dyn std::error::Error + Send + Sync>> {
let mut cmd = match artifact.simulator {
Simulator::Ghdl => {
let mut cmd = Command::new("ghdl");
cmd.args([
"-r",
"--std=08",
"tb",
"--stop-delta=4294967296",
"--unbuffered",
"--",
]);
cmd
}
Simulator::Verilator => Command::new(&artifact.run_target),
};
cmd.args(std::env::args_os());
cmd.current_dir(artifact_dir);
cmd.kill_on_drop(true);

View file

@ -19,8 +19,8 @@ pub async fn ws_handler(socket: WebSocket, refresh_time: Duration) {
return;
};
let artifact_dir = match build::copy_and_build(files).await {
Ok(dir) => dir,
let temp_build = match build::copy_and_build(files).await {
Ok(build) => build,
Err(err) => {
_ = sender
.send(Message::Text(format!("Failed to build: {err}").into()))
@ -29,7 +29,10 @@ pub async fn ws_handler(socket: WebSocket, refresh_time: Duration) {
}
};
let mut process = match run::run(&artifact_dir).await {
let artifact_dir = temp_build.dir;
let artifact = temp_build.artifact;
let mut process = match run::run(&artifact_dir, &artifact).await {
Ok(process) => process,
Err(err) => {
_ = sender
@ -71,7 +74,6 @@ pub async fn ws_handler(socket: WebSocket, refresh_time: Duration) {
out = sout.next_line() => {
match out{
Ok(Some(line)) => {
let msg = ServerMsg::Log {
stream: "stdout",
line: line.strip_prefix(artifact_prefix).unwrap_or(&line),
@ -114,7 +116,7 @@ pub async fn ws_handler(socket: WebSocket, refresh_time: Duration) {
print_deadline += refresh_time;
process.stdin.write_all("\n".as_bytes()).await?;
}
}
}
}
Ok(())
}.await;

View file

@ -162,7 +162,7 @@ impl Handler {
Ok(Some(line)) => {
let msg = if let Some(repr) = line.strip_prefix("led="){
ServerMsg::Led(repr.parse().unwrap_or(0))
}else if let Some(repr) = line.strip_prefix("seg=")
}else if let Some(repr) = line.strip_prefix("seg=")
&& let Some((val, idx)) = repr.split_once(";") {
ServerMsg::Seg{
value: val.parse().unwrap_or(0),
@ -197,15 +197,15 @@ impl Handler {
}
async fn run_program(&mut self) {
match build::build(&self.build_dir, &self.src_dir).await {
Ok(_) => {}
let artifact = match build::build(&self.build_dir, &self.src_dir).await {
Ok(artifact) => artifact,
Err(err) => {
_ = self.eprint(format!("Failed to build: {err}")).await;
return;
}
};
let process = match run::run(&self.build_dir).await {
let process = match run::run(&self.build_dir, &artifact).await {
Ok(process) => process,
Err(err) => {
self.eprint(format!("Failed to run: {err}")).await;
@ -233,8 +233,8 @@ impl Handler {
}
}
pub async fn ws_handler(socket: WebSocket, refresh_time: Duration) {
Handler::workspace(socket, "./target".into(), "./src".into(), refresh_time)
pub async fn ws_handler(socket: WebSocket, workspace_src: PathBuf, refresh_time: Duration) {
Handler::workspace(socket, "./target".into(), workspace_src, refresh_time)
.run()
.await;
}