mirror of
https://github.com/ParkerTenBroeck/hdl_sim.git
synced 2026-06-06 21:24:06 -04:00
added verilog support
This commit is contained in:
parent
5746846896
commit
c3a3e89082
20 changed files with 633 additions and 88 deletions
96
relay/shim/shim.vhdl
Normal file
96
relay/shim/shim.vhdl
Normal 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
35
relay/shim/verilog.c
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
111
relay/ui/app.js
111
relay/ui/app.js
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue