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

96
relay/shim/shim.vhdl Normal file
View file

@ -0,0 +1,96 @@
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb is
end entity;
architecture sim of tb is
signal clk : std_logic := '0';
signal btn : std_logic_vector(31 downto 0) := (others => '0');
signal sw : std_logic_vector(31 downto 0) := (others => '0');
signal led : std_logic_vector(31 downto 0) := (others => '0');
signal segv : std_logic_vector(31 downto 0) := (others => '0');
signal segs : std_logic_vector(31 downto 0) := (others => '0');
procedure ffi_init is
begin
end procedure;
attribute foreign of ffi_init : procedure is
"VHPIDIRECT ffi_init";
function ffi_get_sw return integer is
begin
return 0;
end function;
attribute foreign of ffi_get_sw : function is
"VHPIDIRECT ffi_get_sw";
function ffi_get_btn return integer is
begin
return 0;
end function;
attribute foreign of ffi_get_btn : function is "VHPIDIRECT ffi_get_btn";
procedure ffi_set_outputs(led_i: integer; segv_i: integer; segs_i: integer) is
begin
end procedure;
attribute foreign of ffi_set_outputs : procedure is
"VHPIDIRECT ffi_set_outputs";
function clean_slv(v : std_logic_vector) return std_logic_vector is
variable r : std_logic_vector(v'range);
begin
for i in v'range loop
if v(i) = '1' then
r(i) := '1';
else
r(i) := '0';
end if;
end loop;
return r;
end function;
begin
dut: entity work.circuit
port map (
clk => clk,
btn => btn,
sw => sw,
led => led,
segv => segv,
segs => segs
);
-- 500 Hz clock (2 ms period)
clk <= not clk after 1 ms;
process
variable sw_i : integer;
variable btn_i : integer;
begin
ffi_init;
wait for 0 ns;
while true loop
wait until rising_edge(clk) or falling_edge(clk);
wait for 0 ns;
sw_i := ffi_get_sw;
btn_i := ffi_get_btn;
sw <= std_logic_vector(to_signed(sw_i, 32));
btn <= std_logic_vector(to_signed(btn_i, 32));
ffi_set_outputs(
to_integer(signed(clean_slv(led))),
to_integer(signed(clean_slv(segv))),
to_integer(signed(clean_slv(segs)))
);
end loop;
end process;
end architecture;

35
relay/shim/verilog.c Normal file
View file

@ -0,0 +1,35 @@
#include <cstdint>
#include "Vcircuit.h"
#include "verilated.h"
extern "C" void ffi_init();
extern "C" std::uint32_t ffi_get_sw();
extern "C" std::uint32_t ffi_get_btn();
extern "C" void ffi_set_outputs(std::uint32_t led, std::uint32_t segv, std::uint32_t segs);
int main(int argc, char** argv) {
Verilated::commandArgs(argc, argv);
Vcircuit top;
top.clk = 0;
top.btn = 0;
top.sw = 0;
ffi_init();
while (true) {
top.sw = ffi_get_sw();
top.btn = ffi_get_btn();
top.clk = 0;
top.eval();
ffi_set_outputs(top.led, top.segv, top.segs);
top.sw = ffi_get_sw();
top.btn = ffi_get_btn();
top.clk = 1;
top.eval();
ffi_set_outputs(top.led, top.segv, top.segs);
}
}

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;
}

View file

@ -1,4 +1,6 @@
const LS_KEY_VHDL = "circuit_ui:circuit.vhdl";
const LS_KEY_VERILOG = "circuit_ui:circuit.v";
const LS_KEY_EDITOR_LANGUAGE = "circuit_ui:editor_language";
const LS_KEY_MODE = "circuit_ui:mode";
const EXAMPLE_VHDL_TEXT = `library ieee;
@ -28,6 +30,27 @@ begin
end process;
end description;`;
const EXAMPLE_VERILOG_TEXT = `// Do not modify the following module interface.
module circuit (
input wire clk, // 500 Hz, period 2 ms
input wire [31:0] btn,
input wire [31:0] sw,
output reg [31:0] led = 32'h00000000,
output wire [31:0] segv,
output wire [31:0] segs
);
reg [31:0] counter = 32'h00000000;
assign segv = 32'h00000000;
assign segs = 32'h00000000;
always @(posedge clk) begin
counter <= counter + 32'd1;
led <= counter ^ sw ^ btn;
end
endmodule
`;
function getDomRefs() {
return {
statusPill: document.getElementById("statusPill"),
@ -37,6 +60,7 @@ function getDomRefs() {
editorSection: document.getElementById("editorSection"),
vhdlEditor: document.getElementById("vhdlEditor"),
editorLanguage: document.getElementById("editorLanguage"),
lineGutter: document.getElementById("lineGutter"),
loadExampleBtn: document.getElementById("loadExampleBtn"),
@ -147,9 +171,18 @@ class LogController {
}
class EditorController {
constructor({ editorSection, vhdlEditor, lineGutter, loadExampleBtn, enabled, externalFiles }) {
constructor({
editorSection,
vhdlEditor,
editorLanguage,
lineGutter,
loadExampleBtn,
enabled,
externalFiles,
}) {
this.editorSection = editorSection;
this.vhdlEditor = vhdlEditor;
this.editorLanguage = editorLanguage;
this.lineGutter = lineGutter;
this.loadExampleBtn = loadExampleBtn;
@ -157,6 +190,7 @@ class EditorController {
this.externalFiles = externalFiles && typeof externalFiles === "object" ? externalFiles : null;
this.saveTimer = null;
this.initialized = false;
this.language = "vhdl";
}
init() {
@ -180,23 +214,32 @@ class EditorController {
if (this.initialized) return;
this.initialized = true;
const saved = this.loadFromLocalStorage();
this.vhdlEditor.value = saved !== null ? saved : EXAMPLE_VHDL_TEXT;
this.language = this.loadLanguageFromLocalStorage();
this.editorLanguage.value = this.language;
this.vhdlEditor.value = this.getCurrentBuffer();
this.loadExampleBtn.addEventListener("click", () => {
this.vhdlEditor.value = EXAMPLE_VHDL_TEXT;
this.saveToLocalStorageDebounced();
this.vhdlEditor.value = this.getExampleText(this.language);
this.saveCurrentBufferDebounced();
this.updateLineNumbers();
});
this.vhdlEditor.addEventListener("input", () => {
this.saveToLocalStorageDebounced();
this.saveCurrentBufferDebounced();
this.updateLineNumbers();
});
this.vhdlEditor.addEventListener("scroll", () => {
this.lineGutter.scrollTop = this.vhdlEditor.scrollTop;
});
this.editorLanguage.addEventListener("change", () => {
this.saveCurrentBuffer();
this.language = this.editorLanguage.value === "verilog" ? "verilog" : "vhdl";
this.persistLanguage();
this.vhdlEditor.value = this.getCurrentBuffer();
this.updateLineNumbers();
});
}
getFilesPayload() {
@ -204,9 +247,9 @@ class EditorController {
return this.externalFiles ? { ...this.externalFiles } : {};
}
return {
"circuit.vhdl": this.vhdlEditor.value ?? "",
};
return this.language === "verilog"
? { "circuit.v": this.vhdlEditor.value ?? "" }
: { "circuit.vhdl": this.vhdlEditor.value ?? "" };
}
updateLineNumbers() {
@ -221,27 +264,61 @@ class EditorController {
this.lineGutter.textContent = gutterText;
}
saveToLocalStorageDebounced() {
saveCurrentBufferDebounced() {
if (this.saveTimer) clearTimeout(this.saveTimer);
this.saveTimer = setTimeout(() => {
try {
localStorage.setItem(LS_KEY_VHDL, this.vhdlEditor.value ?? "");
} catch {
// Ignore localStorage failures.
}
this.saveCurrentBuffer();
}, 250);
}
loadFromLocalStorage() {
saveCurrentBuffer() {
const key = this.language === "verilog" ? LS_KEY_VERILOG : LS_KEY_VHDL;
try {
const saved = localStorage.getItem(LS_KEY_VHDL);
localStorage.setItem(key, this.vhdlEditor.value ?? "");
} catch {
// Ignore localStorage failures.
}
}
loadLanguageFromLocalStorage() {
try {
const saved = localStorage.getItem(LS_KEY_EDITOR_LANGUAGE);
if (saved === "verilog" || saved === "vhdl") return saved;
} catch {
// Ignore localStorage failures.
}
return "vhdl";
}
persistLanguage() {
try {
localStorage.setItem(LS_KEY_EDITOR_LANGUAGE, this.language);
} catch {
// Ignore localStorage failures.
}
}
getCurrentBuffer() {
const saved = this.loadBufferFromLocalStorage(this.language);
if (saved !== null) return saved;
return this.getExampleText(this.language);
}
loadBufferFromLocalStorage(language) {
const key = language === "verilog" ? LS_KEY_VERILOG : LS_KEY_VHDL;
try {
const saved = localStorage.getItem(key);
if (saved !== null) return saved;
} catch {
// Ignore localStorage failures.
}
return null;
}
getExampleText(language) {
return language === "verilog" ? EXAMPLE_VERILOG_TEXT : EXAMPLE_VHDL_TEXT;
}
}
class OutputController {

View file

@ -10,8 +10,13 @@
<main class="grid">
<section id="editorSection" class="card editor">
<div class="cardHeader">
<div class="cardTitle">VHDL</div>
<div class="cardTitle">HDL</div>
<div class="cardActions">
<label class="editorLanguageLabel" for="editorLanguage">Language</label>
<select id="editorLanguage" class="editorLanguageSelect" aria-label="Select HDL language">
<option value="vhdl">VHDL</option>
<option value="verilog">Verilog</option>
</select>
<button id="loadExampleBtn" class="secondary">Load example</button>
</div>
</div>

View file

@ -300,6 +300,26 @@ textarea {
background: var(--surface-soft);
}
.editorLanguageLabel {
font-size: 12px;
color: var(--text-muted);
}
.editorLanguageSelect {
min-width: 104px;
padding: 7px 10px;
border: 1px solid var(--border-soft);
border-radius: 10px;
color: var(--text);
background: var(--surface-soft);
font: inherit;
}
.editorLanguageSelect:focus {
outline: 2px solid var(--accent-soft);
outline-offset: 2px;
}
.lineGutter {
width: 54px;
margin: 0;