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

@ -5,6 +5,8 @@
llvmPackages.bintools llvmPackages.bintools
rustup rustup
ghdl-llvm ghdl-llvm
verilator
python3
]; ];
RUSTC_VERSION = "nightly"; RUSTC_VERSION = "nightly";
# https://github.com/rust-lang/rust-bindgen#environment-variables # https://github.com/rust-lang/rust-bindgen#environment-variables

19
examples/example.v Normal file
View file

@ -0,0 +1,19 @@
// 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

View file

@ -12,11 +12,11 @@ pkgs.rustPlatform.buildRustPackage {
cargoBuildFlags = [ "-p" "relay" ]; cargoBuildFlags = [ "-p" "relay" ];
nativeBuildInputs = [ pkgs.makeWrapper ]; nativeBuildInputs = [ pkgs.makeWrapper ];
buildInputs = [ pkgs.ghdl-llvm pkgs.zlib ]; buildInputs = [ pkgs.ghdl-llvm pkgs.verilator pkgs.python3 pkgs.zlib ];
postFixup = '' postFixup = ''
wrapProgram $out/bin/relay \ wrapProgram $out/bin/relay \
--prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.ghdl-llvm pkgs.glib.dev ]} \ --prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.ghdl-llvm pkgs.verilator pkgs.python3 pkgs.glib.dev ]} \
--prefix LIBRARY_PATH : ${pkgs.lib.makeLibraryPath [ pkgs.zlib ]} \ --prefix LIBRARY_PATH : ${pkgs.lib.makeLibraryPath [ pkgs.zlib ]} \
--prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath [ pkgs.zlib ]} --prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath [ pkgs.zlib ]}
''; '';

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; use crate::HResult;
const EMBEDDED_VHDL_UI_LIB: &[u8] = include_bytes!(env!("EMBEDDED_VHDL_CONN_LIB_PATH")); 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>> { async fn ensure_ok(child: Child) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let result = child.wait_with_output().await?; 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(()) 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); pub struct TempDir(PathBuf);
impl Drop for TempDir { impl Drop for TempDir {
fn drop(&mut self) { 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::*; use std::hash::*;
let mut hasher = std::hash::DefaultHasher::default(); let mut hasher = std::hash::DefaultHasher::default();
for (key, value) in &files { 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 hash = hasher.finish();
let mut work_dir = std::env::temp_dir(); 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)?; std::fs::create_dir_all(&work_dir)?;
let work_dir = TempDir(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)?; 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<()> { pub async fn build(build: &Path, src: &Path) -> HResult<BuildArtifact> {
std::fs::create_dir_all(build)?; 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_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_lib_path, EMBEDDED_VHDL_UI_LIB)?;
std::fs::write(&embedded_tb_path, EMBEDDED_TB_VHDL)?; let files = source_files(&src)?;
match detect_simulator(&files)? {
let mut cmd = Command::new("ghdl"); Simulator::Ghdl => build_with_ghdl(&build, &files, &embedded_lib_path).await,
cmd.kill_on_drop(true); Simulator::Verilator => build_with_verilator(&build, &files, &embedded_lib_path).await,
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()?);
}
} }
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 serde::{Deserialize, Serialize};
use std::{ use std::{
net::{IpAddr, SocketAddr}, net::{IpAddr, SocketAddr}, path::PathBuf, time::Duration
time::Duration,
}; };
pub mod build; pub mod build;
@ -45,12 +44,13 @@ async fn not_found() -> impl IntoResponse {
(StatusCode::NOT_FOUND, "not found") (StatusCode::NOT_FOUND, "not found")
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Debug)]
struct Config { struct Config {
ip: IpAddr, ip: IpAddr,
port: u16, port: u16,
update_ms: u64, update_ms: u64,
workspace_ws: bool, workspace_ws: bool,
workspace_src: PathBuf,
} }
impl Default for Config { impl Default for Config {
@ -59,7 +59,8 @@ impl Default for Config {
ip: IpAddr::from([127, 0, 0, 1]), ip: IpAddr::from([127, 0, 0, 1]),
port: 8080, port: 8080,
update_ms: 30, 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" => { "--workspace" => {
cfg.workspace_ws = true; 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" => { "--help" | "-h" => {
return Err( return Err(
"usage: relay [--ip <ip>] [--port <port>] [--update-ms <ms>] [--workspace]" "usage: relay [--ip <ip>] [--port <port>] [--update-ms <ms>] [--workspace]"
@ -144,8 +149,9 @@ async fn main() {
"/ws/workspace", "/ws/workspace",
get(move |ws: WebSocketUpgrade| { get(move |ws: WebSocketUpgrade| {
let update_interval = update_interval; let update_interval = update_interval;
let workspace_src = cfg.workspace_src;
async move { 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 tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
use crate::build::{BuildArtifact, Simulator};
pub struct Process { pub struct Process {
pub child: Child, pub child: Child,
pub stdin: ChildStdin, pub stdin: ChildStdin,
@ -9,16 +11,26 @@ pub struct Process {
pub stderr: ChildStderr, pub stderr: ChildStderr,
} }
pub async fn run(artifact_dir: &Path) -> Result<Process, Box<dyn std::error::Error + Send + Sync>> { pub async fn run(
let mut cmd = Command::new("ghdl"); artifact_dir: &Path,
cmd.args([ artifact: &BuildArtifact,
"-r", ) -> Result<Process, Box<dyn std::error::Error + Send + Sync>> {
"--std=08", let mut cmd = match artifact.simulator {
"tb", Simulator::Ghdl => {
"--stop-delta=4294967296", let mut cmd = Command::new("ghdl");
"--unbuffered", 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.args(std::env::args_os());
cmd.current_dir(artifact_dir); cmd.current_dir(artifact_dir);
cmd.kill_on_drop(true); cmd.kill_on_drop(true);

View file

@ -19,8 +19,8 @@ pub async fn ws_handler(socket: WebSocket, refresh_time: Duration) {
return; return;
}; };
let artifact_dir = match build::copy_and_build(files).await { let temp_build = match build::copy_and_build(files).await {
Ok(dir) => dir, Ok(build) => build,
Err(err) => { Err(err) => {
_ = sender _ = sender
.send(Message::Text(format!("Failed to build: {err}").into())) .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, Ok(process) => process,
Err(err) => { Err(err) => {
_ = sender _ = sender
@ -71,7 +74,6 @@ pub async fn ws_handler(socket: WebSocket, refresh_time: Duration) {
out = sout.next_line() => { out = sout.next_line() => {
match out{ match out{
Ok(Some(line)) => { Ok(Some(line)) => {
let msg = ServerMsg::Log { let msg = ServerMsg::Log {
stream: "stdout", stream: "stdout",
line: line.strip_prefix(artifact_prefix).unwrap_or(&line), line: line.strip_prefix(artifact_prefix).unwrap_or(&line),

View file

@ -197,15 +197,15 @@ impl Handler {
} }
async fn run_program(&mut self) { async fn run_program(&mut self) {
match build::build(&self.build_dir, &self.src_dir).await { let artifact = match build::build(&self.build_dir, &self.src_dir).await {
Ok(_) => {} Ok(artifact) => artifact,
Err(err) => { Err(err) => {
_ = self.eprint(format!("Failed to build: {err}")).await; _ = self.eprint(format!("Failed to build: {err}")).await;
return; return;
} }
}; };
let process = match run::run(&self.build_dir).await { let process = match run::run(&self.build_dir, &artifact).await {
Ok(process) => process, Ok(process) => process,
Err(err) => { Err(err) => {
self.eprint(format!("Failed to run: {err}")).await; self.eprint(format!("Failed to run: {err}")).await;
@ -233,8 +233,8 @@ impl Handler {
} }
} }
pub async fn ws_handler(socket: WebSocket, refresh_time: Duration) { pub async fn ws_handler(socket: WebSocket, workspace_src: PathBuf, refresh_time: Duration) {
Handler::workspace(socket, "./target".into(), "./src".into(), refresh_time) Handler::workspace(socket, "./target".into(), workspace_src, refresh_time)
.run() .run()
.await; .await;
} }

View file

@ -1,4 +1,6 @@
const LS_KEY_VHDL = "circuit_ui:circuit.vhdl"; 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 LS_KEY_MODE = "circuit_ui:mode";
const EXAMPLE_VHDL_TEXT = `library ieee; const EXAMPLE_VHDL_TEXT = `library ieee;
@ -28,6 +30,27 @@ begin
end process; end process;
end description;`; 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() { function getDomRefs() {
return { return {
statusPill: document.getElementById("statusPill"), statusPill: document.getElementById("statusPill"),
@ -37,6 +60,7 @@ function getDomRefs() {
editorSection: document.getElementById("editorSection"), editorSection: document.getElementById("editorSection"),
vhdlEditor: document.getElementById("vhdlEditor"), vhdlEditor: document.getElementById("vhdlEditor"),
editorLanguage: document.getElementById("editorLanguage"),
lineGutter: document.getElementById("lineGutter"), lineGutter: document.getElementById("lineGutter"),
loadExampleBtn: document.getElementById("loadExampleBtn"), loadExampleBtn: document.getElementById("loadExampleBtn"),
@ -147,9 +171,18 @@ class LogController {
} }
class EditorController { class EditorController {
constructor({ editorSection, vhdlEditor, lineGutter, loadExampleBtn, enabled, externalFiles }) { constructor({
editorSection,
vhdlEditor,
editorLanguage,
lineGutter,
loadExampleBtn,
enabled,
externalFiles,
}) {
this.editorSection = editorSection; this.editorSection = editorSection;
this.vhdlEditor = vhdlEditor; this.vhdlEditor = vhdlEditor;
this.editorLanguage = editorLanguage;
this.lineGutter = lineGutter; this.lineGutter = lineGutter;
this.loadExampleBtn = loadExampleBtn; this.loadExampleBtn = loadExampleBtn;
@ -157,6 +190,7 @@ class EditorController {
this.externalFiles = externalFiles && typeof externalFiles === "object" ? externalFiles : null; this.externalFiles = externalFiles && typeof externalFiles === "object" ? externalFiles : null;
this.saveTimer = null; this.saveTimer = null;
this.initialized = false; this.initialized = false;
this.language = "vhdl";
} }
init() { init() {
@ -180,23 +214,32 @@ class EditorController {
if (this.initialized) return; if (this.initialized) return;
this.initialized = true; this.initialized = true;
const saved = this.loadFromLocalStorage(); this.language = this.loadLanguageFromLocalStorage();
this.vhdlEditor.value = saved !== null ? saved : EXAMPLE_VHDL_TEXT; this.editorLanguage.value = this.language;
this.vhdlEditor.value = this.getCurrentBuffer();
this.loadExampleBtn.addEventListener("click", () => { this.loadExampleBtn.addEventListener("click", () => {
this.vhdlEditor.value = EXAMPLE_VHDL_TEXT; this.vhdlEditor.value = this.getExampleText(this.language);
this.saveToLocalStorageDebounced(); this.saveCurrentBufferDebounced();
this.updateLineNumbers(); this.updateLineNumbers();
}); });
this.vhdlEditor.addEventListener("input", () => { this.vhdlEditor.addEventListener("input", () => {
this.saveToLocalStorageDebounced(); this.saveCurrentBufferDebounced();
this.updateLineNumbers(); this.updateLineNumbers();
}); });
this.vhdlEditor.addEventListener("scroll", () => { this.vhdlEditor.addEventListener("scroll", () => {
this.lineGutter.scrollTop = this.vhdlEditor.scrollTop; 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() { getFilesPayload() {
@ -204,9 +247,9 @@ class EditorController {
return this.externalFiles ? { ...this.externalFiles } : {}; return this.externalFiles ? { ...this.externalFiles } : {};
} }
return { return this.language === "verilog"
"circuit.vhdl": this.vhdlEditor.value ?? "", ? { "circuit.v": this.vhdlEditor.value ?? "" }
}; : { "circuit.vhdl": this.vhdlEditor.value ?? "" };
} }
updateLineNumbers() { updateLineNumbers() {
@ -221,27 +264,61 @@ class EditorController {
this.lineGutter.textContent = gutterText; this.lineGutter.textContent = gutterText;
} }
saveToLocalStorageDebounced() { saveCurrentBufferDebounced() {
if (this.saveTimer) clearTimeout(this.saveTimer); if (this.saveTimer) clearTimeout(this.saveTimer);
this.saveTimer = setTimeout(() => { this.saveTimer = setTimeout(() => {
try { this.saveCurrentBuffer();
localStorage.setItem(LS_KEY_VHDL, this.vhdlEditor.value ?? "");
} catch {
// Ignore localStorage failures.
}
}, 250); }, 250);
} }
loadFromLocalStorage() { saveCurrentBuffer() {
const key = this.language === "verilog" ? LS_KEY_VERILOG : LS_KEY_VHDL;
try { 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; if (saved !== null) return saved;
} catch { } catch {
// Ignore localStorage failures. // Ignore localStorage failures.
} }
return null; return null;
} }
getExampleText(language) {
return language === "verilog" ? EXAMPLE_VERILOG_TEXT : EXAMPLE_VHDL_TEXT;
}
} }
class OutputController { class OutputController {

View file

@ -10,8 +10,13 @@
<main class="grid"> <main class="grid">
<section id="editorSection" class="card editor"> <section id="editorSection" class="card editor">
<div class="cardHeader"> <div class="cardHeader">
<div class="cardTitle">VHDL</div> <div class="cardTitle">HDL</div>
<div class="cardActions"> <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> <button id="loadExampleBtn" class="secondary">Load example</button>
</div> </div>
</div> </div>

View file

@ -300,6 +300,26 @@ textarea {
background: var(--surface-soft); 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 { .lineGutter {
width: 54px; width: 54px;
margin: 0; margin: 0;

101
src_verilog/bcd.v Normal file
View file

@ -0,0 +1,101 @@
module bcd (
input wire clk,
input wire signed [22:0] num, // sfixed(15 downto -7)
input wire en,
output reg [63:0] seg
);
function [7:0] seg_encode;
input [3:0] d;
begin
case (d)
4'd0: seg_encode = 8'b00111111;
4'd1: seg_encode = 8'b00000110;
4'd2: seg_encode = 8'b01011011;
4'd3: seg_encode = 8'b01001111;
4'd4: seg_encode = 8'b01100110;
4'd5: seg_encode = 8'b01101101;
4'd6: seg_encode = 8'b01111101;
4'd7: seg_encode = 8'b00000111;
4'd8: seg_encode = 8'b01111111;
4'd9: seg_encode = 8'b01101111;
default: seg_encode = 8'b00000000;
endcase
end
endfunction
function [3:0] to_bcd_digit;
input integer value;
integer remainder;
begin
remainder = value % 10;
case (remainder)
0: to_bcd_digit = 4'd0;
1: to_bcd_digit = 4'd1;
2: to_bcd_digit = 4'd2;
3: to_bcd_digit = 4'd3;
4: to_bcd_digit = 4'd4;
5: to_bcd_digit = 4'd5;
6: to_bcd_digit = 4'd6;
7: to_bcd_digit = 4'd7;
8: to_bcd_digit = 4'd8;
9: to_bcd_digit = 4'd9;
default: to_bcd_digit = 4'd0;
endcase
end
endfunction
integer scaled_hundredths;
integer magnitude;
integer frac_hundredths;
integer tmp;
integer j;
reg negative;
reg [3:0] digits [0:7];
reg [63:0] out_seg;
always @* begin
if (!en) begin
seg = 64'b0;
end else begin
out_seg = 64'b0;
// num is Q16.7 fixed-point, so value = num / 128.
// Round to nearest hundredth.
if (num < 0) begin
scaled_hundredths = ((num * 100) - 64) / 128;
end else begin
scaled_hundredths = ((num * 100) + 64) / 128;
end
negative = (scaled_hundredths < 0);
if (negative) begin
magnitude = -scaled_hundredths;
end else begin
magnitude = scaled_hundredths;
end
frac_hundredths = magnitude % 100;
tmp = magnitude;
for (j = 0; j < 8; j = j + 1) begin
digits[j] = to_bcd_digit(tmp);
tmp = tmp / 10;
end
for (j = 0; j < 7; j = j + 1) begin
out_seg[(7 - j) * 8 +: 8] = seg_encode(digits[j]);
end
out_seg[5 * 8 + 7] = 1'b1;
if (negative) begin
out_seg[6] = 1'b1;
end
seg = out_seg;
end
end
endmodule

56
src_verilog/example.v Normal file
View file

@ -0,0 +1,56 @@
// 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 wire [31:0] led,
output wire [31:0] segv,
output wire [31:0] segs
);
wire [3:0] dig;
wire dig_e;
wire dot;
wire eq;
wire [3:0] op;
wire op_e;
wire [63:0] seg_a;
wire [2:0] segs_mux;
wire signed [22:0] sw_fixed;
assign led = 32'b0;
assign segs = {29'b0, segs_mux};
// Convert signed 16-bit integer to signed Q16.7 fixed-point.
assign sw_fixed = {sw[15:0], 7'b0};
keypad_input keypad_input_inst (
.clk(clk),
.keypad(btn[15:8]),
.dig(dig),
.dig_e(dig_e),
.dot(dot),
.eq(eq),
.op(op),
.op_e(op_e)
);
bcd bcd_inst (
.clk(clk),
.num(sw_fixed),
.en(1'b1),
.seg(seg_a)
);
seg_plex seg_plex_inst (
.clk(clk),
.seg0(seg_a),
.seg1(64'h0000000000000000),
.seg2(64'h0000000000000000),
.seg3(64'h0000000000000000),
.segv(segv),
.segs(segs_mux)
);
endmodule

View file

@ -0,0 +1,55 @@
module keypad_input (
input wire clk,
input wire [7:0] keypad,
output reg [3:0] dig,
output reg dig_e,
output reg dot,
output reg eq,
output reg [3:0] op,
output reg op_e
);
reg [7:0] keypad_curr = 8'b00000000;
always @(posedge clk or negedge clk) begin
if (clk && (keypad != keypad_curr)) begin
case (keypad)
8'b10001000: begin op <= 4'b0001; op_e <= 1'b1; end
8'b10000100: begin eq <= 1'b1; end
8'b10000010: begin dot <= 1'b1; end
8'b10000001: begin dig <= 4'b0000; dig_e <= 1'b1; end
8'b01001000: begin op <= 4'b0010; op_e <= 1'b1; end
8'b01000100: begin dig <= 4'b0011; dig_e <= 1'b1; end
8'b01000010: begin dig <= 4'b0010; dig_e <= 1'b1; end
8'b01000001: begin dig <= 4'b0001; dig_e <= 1'b1; end
8'b00101000: begin op <= 4'b0011; op_e <= 1'b1; end
8'b00100100: begin dig <= 4'b0110; dig_e <= 1'b1; end
8'b00100010: begin dig <= 4'b0101; dig_e <= 1'b1; end
8'b00100001: begin dig <= 4'b0100; dig_e <= 1'b1; end
8'b00011000: begin op <= 4'b0100; op_e <= 1'b1; end
8'b00010100: begin dig <= 4'b1000; dig_e <= 1'b1; end
8'b00010010: begin dig <= 4'b1000; dig_e <= 1'b1; end
8'b00010001: begin dig <= 4'b0111; dig_e <= 1'b1; end
default: begin end
endcase
keypad_curr <= keypad;
end
if (!clk && (keypad != keypad_curr)) begin
dig <= 4'b0000;
dig_e <= 1'b0;
eq <= 1'b0;
dot <= 1'b0;
op <= 4'b0000;
op_e <= 1'b0;
end
end
endmodule

32
src_verilog/seg_plex.v Normal file
View file

@ -0,0 +1,32 @@
module seg_plex (
input wire clk,
input wire [63:0] seg0,
input wire [63:0] seg1,
input wire [63:0] seg2,
input wire [63:0] seg3,
output reg [31:0] segv,
output reg [2:0] segs
);
reg [2:0] counter = 3'b000;
always @(posedge clk) begin
case (counter)
3'd0: segv <= seg0[31:0];
3'd1: segv <= seg0[63:32];
3'd2: segv <= seg1[31:0];
3'd3: segv <= seg1[63:32];
3'd4: segv <= seg2[31:0];
3'd5: segv <= seg2[63:32];
3'd6: segv <= seg3[31:0];
3'd7: segv <= seg3[63:32];
default: segv <= 32'b0;
endcase
counter <= counter + 3'd1;
segs <= counter;
end
endmodule