├── .github └── workflows │ └── main.yml ├── .gitignore ├── Makefile ├── README.md ├── doc └── capture-eurorack-pmod.gif ├── plugin.json ├── res └── eurorack-pmod-panel.svg ├── rtl └── core.sv └── src ├── eurorack-pmod.cpp ├── plugin.cpp └── plugin.hpp /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | ubuntu-build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: YosysHQ/setup-oss-cad-suite@v3 11 | - run: verilator --version 12 | - run: wget https://vcvrack.com/downloads/Rack-SDK-2.4.1-lin-x64.zip 13 | - run: unzip Rack-SDK*.zip && rm Rack-SDK*.zip 14 | - run: make RACK_DIR=`pwd`/Rack-SDK 15 | - run: make RACK_DIR=`pwd`/Rack-SDK install 16 | - uses: actions/upload-artifact@v4 17 | with: 18 | name: plugin-lin-x64 19 | path: dist/ 20 | 21 | macos-build: 22 | runs-on: macos-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - run: brew install verilator 26 | - run: verilator --version 27 | - run: wget https://vcvrack.com/downloads/Rack-SDK-2.4.1-mac-x64.zip 28 | - run: unzip Rack-SDK*.zip && rm Rack-SDK*.zip 29 | - run: make RACK_DIR=`pwd`/Rack-SDK 30 | - run: make RACK_DIR=`pwd`/Rack-SDK install 31 | - uses: actions/upload-artifact@v4 32 | with: 33 | name: plugin-mac-x64 34 | path: dist/ 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | /plugin.so 4 | /plugin.dylib 5 | /plugin.dll 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # If RACK_DIR is not defined when calling the Makefile, default to two directories above 2 | RACK_DIR ?= ../.. 3 | 4 | VERILATOR_ROOT=$(shell verilator -getenv VERILATOR_ROOT) 5 | # Where to build the verilated RTL. 6 | VERILATED_PATH=build/rtl-verilated 7 | # Files generated by verilator that we must execute / link to. 8 | VERILATED_MK=Vcore.mk 9 | VERILATED_OBJ=Vcore__ALL.a 10 | 11 | # FLAGS will be passed to both the C and C++ compiler 12 | FLAGS += -I$(VERILATOR_ROOT)/include -faligned-new 13 | 14 | # Careful about linking to shared libraries, since you can't assume much about the user's environment and library search path. 15 | # Static libraries are fine, but they should be added to this plugin's build system. 16 | OBJECTS += $(VERILATED_PATH)/$(VERILATED_OBJ) 17 | 18 | # Add .cpp files to the build 19 | SOURCES += $(wildcard src/*.cpp) \ 20 | $(VERILATOR_ROOT)/include/verilated.cpp \ 21 | $(VERILATOR_ROOT)/include/verilated_threads.cpp 22 | 23 | # Add files to the ZIP package when running `make dist` 24 | # The compiled plugin and "plugin.json" are automatically added. 25 | DISTRIBUTABLES += res 26 | DISTRIBUTABLES += $(wildcard LICENSE*) 27 | DISTRIBUTABLES += $(wildcard presets) 28 | 29 | $(VERILATED_PATH)/Vcore.mk: rtl/core.sv 30 | mkdir -p build; \ 31 | verilator --cc $< -Mdir $(VERILATED_PATH) -CFLAGS -fPIC 32 | 33 | $(VERILATED_PATH)/$(VERILATED_OBJ): $(VERILATED_PATH)/Vcore.mk 34 | make -C $(VERILATED_PATH) -f $(VERILATED_MK) 35 | 36 | src/eurorack-pmod.cpp: $(VERILATED_PATH)/$(VERILATED_OBJ) 37 | 38 | # Include the Rack plugin Makefile framework 39 | include $(RACK_DIR)/plugin.mk 40 | 41 | CXXFLAGS := $(filter-out -std=c++11,$(CXXFLAGS)) 42 | CXXFLAGS += -std=c++17 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ci workflow](https://github.com/apfelaudio/verilog-vcvrack/actions/workflows/main.yml/badge.svg) 2 | 3 | # Basic example of simulating Verilog inside a VCV Rack plugin. 4 | 5 | This plugin simulates the panel layout of the [`eurorack-pmod`](https://github.com/schnommus/eurorack-pmod) FPGA-based module. See [`rtl/core.sv`](rtl/core.sv) for the Verilog source. This example is intentionally kept as simple as possible. 6 | 7 | ![eurorack-pmod running inside VCV rack](doc/capture-eurorack-pmod.gif) 8 | 9 | # Dependencies 10 | 11 | 1. Install VCV rack binaries or build it from source. 12 | 2. Install [VCV rack SDK](https://vcvrack.com/manual/PluginDevelopmentTutorial) (or just put this folder in the plugins/ directory of Rack if you are building from source) 13 | 3. Install Verilator (used to build a C++ simulation from the Verilog core). You can either get it from your package manager or use the one included in the [oss-cad-suite](https://github.com/YosysHQ/oss-cad-suite-build#installation). 14 | 15 | # Building and running 16 | 17 | If you are using the SDK, make sure you have `RACK_DIR` set: 18 | 19 | ``` 20 | export RACK_DIR= 21 | ``` 22 | 23 | From the root directory of this repository: 24 | 25 | ``` 26 | $ make 27 | $ make install 28 | ``` 29 | 30 | This plugin should now be visible on restarting VCV Rack. 31 | 32 | # Limitations 33 | 34 | At the moment only the audio rate `sample_clk` is injected into the verilog core, I doubt verilator would be fast enough to simulate filters pipelined at the PLL clock (12MHz/24MHz). 35 | 36 | # Debugging 37 | 38 | Some basic CI for Mac + Linux is in [`.github/workflows/main.yml`](.github/workflows/main.yml), this may be useful to look at if you are having trouble building. 39 | 40 | Sometimes it is useful to run VCVRack in development mode so that you get some more detailed logs if a plugin doesn't load correctly: 41 | 42 | ```bash 43 | $ ./Rack --dev 44 | ``` 45 | 46 | For example, an update to verilator recently added an extra source dependency that was missing, causing an error like this -- 47 | 48 | 49 | ```bash 50 | [0.011 info src/plugin.cpp:130 loadPlugin] Loading plugin from /home/seb/Downloads/Rack2Free/plugins/eurorack-pmod-vcvrack 51 | [0.011 warn src/plugin.cpp:196 loadPlugin] Could not load plugin /home/seb/Downloads/Rack2Free/plugins/eurorack-pmod-vcvrack: Failed to load library /home/seb/Downloads/Rack2Free/plugins/eurorack-pmod-vcvrack/plugin.so: /home/seb/Downloads/Rack2Free/plugins/eurorack-pmod-vcvrack/plugin.so: undefined symbol: _ZN12VlThreadPoolC1EP16VerilatedContextj 52 | ``` 53 | -------------------------------------------------------------------------------- /doc/capture-eurorack-pmod.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apfaudio/verilog-vcvrack/15f349848b24f737f6059d2425bf4a0abbfc9866/doc/capture-eurorack-pmod.gif -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "slug": "eurorack-pmod-vcvrack", 3 | "name": "eurorack-pmod-vcvrack", 4 | "version": "2.0.0", 5 | "license": "CERN-OHL-S-2.0", 6 | "brand": "vk2.berlin", 7 | "author": "Sebastian Holzapfel", 8 | "authorEmail": "me@sebholzapfel.com", 9 | "authorUrl": "sebholzapfel.com", 10 | "pluginUrl": "vk2.berlin", 11 | "manualUrl": "", 12 | "sourceUrl": "", 13 | "donateUrl": "", 14 | "changelogUrl": "", 15 | "modules": [ 16 | { 17 | "slug": "eurorack-pmod", 18 | "name": "eurorack-pmod", 19 | "description": "Emulator for FPGA-based multifunction module.", 20 | "tags": [ 21 | "Hardware" 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /res/eurorack-pmod-panel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 43 | 50 | 51 | 55 | 59 | 63 | 67 | 71 | 75 | 76 | 80 | 84 | 85 | 89 | 93 | 97 | 101 | 105 | 106 | 110 | 114 | 118 | 122 | 126 | 127 | 131 | 135 | 139 | 140 | 144 | 148 | 152 | 156 | 160 | 161 | 165 | 169 | 170 | 174 | 178 | 182 | 183 | 187 | 191 | 195 | 199 | 203 | 204 | 208 | 212 | 213 | 217 | 221 | 222 | 226 | 230 | 231 | 235 | 239 | 243 | 247 | 248 | 252 | 256 | 260 | 264 | 268 | 269 | 273 | 277 | 278 | 282 | 286 | 290 | 294 | 295 | 299 | 303 | 307 | 311 | 315 | 316 | 320 | 324 | 325 | 329 | 333 | 337 | 341 | 342 | 346 | 350 | 354 | 358 | 362 | 363 | 367 | 371 | 372 | 376 | 380 | 384 | 388 | 392 | 393 | 397 | 401 | 402 | 406 | 410 | 414 | 415 | 419 | 423 | 427 | 428 | 432 | 436 | 440 | 444 | 448 | 449 | 453 | 457 | 458 | 462 | 466 | 470 | 474 | 478 | 479 | 483 | 487 | 488 | 492 | 496 | 500 | 504 | 508 | 509 | 513 | 517 | 518 | 522 | 526 | 530 | 534 | 538 | 542 | 546 | 547 | 551 | 555 | 556 | 560 | 564 | γ 566 | 570 | 574 | 575 | 579 | 582 | 583 | 587 | α 589 | 593 | 594 | 598 | 601 | 602 | 606 | δ 608 | 612 | 613 | 614 | 618 | 622 | 625 | 628 | 631 | 634 | 637 | 640 | 641 | 645 | BRIDGE 647 | 651 | 655 | 659 | 663 | 667 | 671 | 675 | 679 | 680 | 681 | 685 | 689 | 692 | 693 | 697 | γ 699 | 703 | 707 | 708 | 712 | 715 | 716 | 720 | β 722 | 726 | 730 | 731 | 735 | 738 | 739 | 743 | β 745 | 749 | 753 | 754 | 758 | 761 | 762 | 766 | δ 768 | 772 | 773 | 774 | 778 | 782 | 785 | 788 | 791 | 792 | 796 | VK2 798 | 802 | 806 | 810 | 814 | 818 | 819 | 823 | 826 | 829 | 832 | 833 | 837 | SEB 839 | 843 | 847 | 851 | 855 | 856 | 857 | 861 | 865 | 868 | 869 | 873 | α 875 | 879 | 880 | 881 | 885 | 889 | 892 | 895 | 898 | 901 | 902 | 906 | PMOD 908 | 912 | 916 | 920 | 924 | 925 | 928 | 931 | 932 | 935 | 938 | 939 | 942 | 945 | 946 | 949 | 952 | 953 | 954 | 955 | 1089 | 1090 | -------------------------------------------------------------------------------- /rtl/core.sv: -------------------------------------------------------------------------------- 1 | // Clock Divider 2 | // 3 | // Given an input clock source on input 0, produce divided output on Output 0 - 3. 4 | // 5 | // Mapping: 6 | // - Input 0: Clock input (Hi > 2V, Lo < 0.5V) 7 | // - Input 1-3: Not used 8 | // - Output 0: Clock * 1 (mirrored) 9 | // - Output 1: Clock / 2 (Hi == 5V, Lo == 0V) 10 | // - Output 2: Clock / 4 11 | // - Output 3: Clock / 8 12 | 13 | module clkdiv #( 14 | parameter W = 16, 15 | parameter FP_OFFSET = 2 16 | )( 17 | input clk, 18 | input sample_clk, 19 | input signed [W-1:0] sample_in0, 20 | input signed [W-1:0] sample_in1, 21 | input signed [W-1:0] sample_in2, 22 | input signed [W-1:0] sample_in3, 23 | output signed [W-1:0] sample_out0, 24 | output signed [W-1:0] sample_out1, 25 | output signed [W-1:0] sample_out2, 26 | output signed [W-1:0] sample_out3 27 | ); 28 | 29 | // Calibrated samples represent millivolts in 16 bits, last 2 bits are fractional. 30 | `define FROM_MV(value) (value <<< FP_OFFSET) 31 | 32 | // Input and output voltage thresholds. 33 | localparam SCHMITT_HI = `FROM_MV(2000); 34 | localparam SCHMITT_LO = `FROM_MV(500); 35 | localparam OUT_HI = `FROM_MV(5000); 36 | localparam OUT_LO = `FROM_MV(0); 37 | 38 | // Keeping track of last input state effectively behaves as schmitt inputs. 39 | logic last_state_hi = 1'b0; 40 | logic [3:0] div = 0; 41 | 42 | always_ff @(posedge sample_clk) begin 43 | if (sample_in0 > SCHMITT_HI && !last_state_hi) begin 44 | last_state_hi <= 1'b1; 45 | // Increment count on every rising edge. 46 | div <= div + 1; 47 | end else if (sample_in0 < SCHMITT_LO && last_state_hi) begin 48 | last_state_hi <= 1'b0; 49 | end 50 | end 51 | 52 | // output 0 mirrors input 0, outputs 1-3 are /2, /4, /8 53 | assign sample_out0 = sample_in0; 54 | assign sample_out1 = div[0] ? OUT_HI : OUT_LO; 55 | assign sample_out2 = div[1] ? OUT_HI : OUT_LO; 56 | assign sample_out3 = div[2] ? OUT_HI : OUT_LO; 57 | 58 | endmodule 59 | -------------------------------------------------------------------------------- /src/eurorack-pmod.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "plugin.hpp" 4 | 5 | // Verilator must run and build the verilated RTL before this 6 | // source file is compiled. The Makefile handles this. 7 | #include "../build/rtl-verilated/Vcore.h" 8 | 9 | struct Eurorack_pmod : Module { 10 | enum ParamId { 11 | PARAMS_LEN 12 | }; 13 | enum InputId { 14 | IN0_INPUT, 15 | IN1_INPUT, 16 | IN2_INPUT, 17 | IN3_INPUT, 18 | INPUTS_LEN 19 | }; 20 | enum OutputId { 21 | OUT0_OUTPUT, 22 | OUT1_OUTPUT, 23 | OUT2_OUTPUT, 24 | OUT3_OUTPUT, 25 | OUTPUTS_LEN 26 | }; 27 | enum LightId { 28 | ENUMS(LED_IN0_LIGHT, 2), 29 | ENUMS(LED_IN1_LIGHT, 2), 30 | ENUMS(LED_IN2_LIGHT, 2), 31 | ENUMS(LED_IN3_LIGHT, 2), 32 | ENUMS(LED_OUT0_LIGHT, 2), 33 | ENUMS(LED_OUT1_LIGHT, 2), 34 | ENUMS(LED_OUT2_LIGHT, 2), 35 | ENUMS(LED_OUT3_LIGHT, 2), 36 | LIGHTS_LEN 37 | }; 38 | 39 | Vcore *tb; 40 | 41 | Eurorack_pmod() { 42 | config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); 43 | configInput(IN0_INPUT, ""); 44 | configInput(IN1_INPUT, ""); 45 | configInput(IN2_INPUT, ""); 46 | configInput(IN3_INPUT, ""); 47 | configOutput(OUT1_OUTPUT, ""); 48 | configOutput(OUT2_OUTPUT, ""); 49 | configOutput(OUT3_OUTPUT, ""); 50 | configOutput(OUT0_OUTPUT, ""); 51 | 52 | tb = new Vcore(); 53 | } 54 | 55 | /* The eurorack-pmod gateware expects 16-bit signed values representing 56 | * millivolts in fixed-point 14.2 layout for samples coming in/out. */ 57 | int16_t volt_to_fp16(float v, uint8_t fractional_bits = 2, 58 | int16_t clamp_hi = SHRT_MAX, int16_t clamp_lo = SHRT_MIN) { 59 | v *= 1e3 * (float)(1 << fractional_bits); 60 | if (v >= clamp_hi) return clamp_hi; 61 | if (v <= clamp_lo) return clamp_lo; 62 | return uint16_t(v); 63 | } 64 | 65 | float fp16_to_volt(int16_t b, uint8_t fractional_bits = 2) { 66 | float v = (float)b; 67 | v /= 1e3 * (float)(1 << fractional_bits); 68 | return v; 69 | } 70 | 71 | float volt_to_led_green(float f) { 72 | float brightness = f / 5.0; 73 | brightness = ((brightness > 1.0) ? 1.0 : brightness); 74 | brightness = ((brightness < 0.0) ? 0.0 : brightness); 75 | return brightness; 76 | } 77 | 78 | float volt_to_led_red(float f) { 79 | float brightness = -f / 5.0; 80 | brightness = ((brightness > 1.0) ? 1.0 : brightness); 81 | brightness = ((brightness < 0.0) ? 0.0 : brightness); 82 | return brightness; 83 | } 84 | 85 | void process(const ProcessArgs& args) override { 86 | tb->sample_clk = 0; 87 | tb->eval(); 88 | 89 | int in_ix = 0; 90 | for (auto* in : {&tb->sample_in0, &tb->sample_in1, 91 | &tb->sample_in2, &tb->sample_in3}) { 92 | *in = volt_to_fp16(inputs[IN0_INPUT+in_ix].getVoltage()); 93 | lights[LED_IN0_LIGHT + (in_ix*2) + 0].setBrightness( 94 | volt_to_led_green(inputs[IN0_INPUT+in_ix].getVoltage())); 95 | lights[LED_IN0_LIGHT + (in_ix*2) + 1].setBrightness( 96 | volt_to_led_red(inputs[IN0_INPUT+in_ix].getVoltage())); 97 | ++in_ix; 98 | } 99 | 100 | int out_ix = 0; 101 | for (auto* out : {&tb->sample_out0, &tb->sample_out1, 102 | &tb->sample_out2, &tb->sample_out3}) { 103 | outputs[OUT0_OUTPUT+out_ix].setVoltage(fp16_to_volt(*out)); 104 | lights[LED_OUT0_LIGHT + (out_ix*2) + 0].setBrightness( 105 | volt_to_led_green(outputs[OUT0_OUTPUT+out_ix].getVoltage())); 106 | lights[LED_OUT0_LIGHT + (out_ix*2) + 1].setBrightness( 107 | volt_to_led_red(outputs[OUT0_OUTPUT+out_ix].getVoltage())); 108 | ++out_ix; 109 | } 110 | 111 | tb->sample_clk = 1; 112 | tb->eval(); 113 | } 114 | }; 115 | 116 | RectangleLight * createRectLight(Eurorack_pmod* module, float pos_x, float pos_y, int light) { 117 | auto l = createLight>(mm2px(Vec(pos_x, pos_y)), module, light); 118 | l->box.size = mm2px(Vec(8.0, 1.0)); 119 | return l; 120 | } 121 | 122 | struct Eurorack_pmodWidget : ModuleWidget { 123 | Eurorack_pmodWidget(Eurorack_pmod* module) { 124 | setModule(module); 125 | setPanel(createPanel(asset::plugin(pluginInstance, "res/eurorack-pmod-panel.svg"))); 126 | 127 | addChild(createWidget(Vec(RACK_GRID_WIDTH/4, 0))); 128 | addChild(createWidget(Vec(RACK_GRID_WIDTH/4, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 129 | 130 | addInput(createInputCentered(mm2px(Vec(9.7, 15.0)), module, Eurorack_pmod::IN0_INPUT)); 131 | addInput(createInputCentered(mm2px(Vec(9.7, 28.8)), module, Eurorack_pmod::IN1_INPUT)); 132 | addInput(createInputCentered(mm2px(Vec(9.7, 42.5)), module, Eurorack_pmod::IN2_INPUT)); 133 | addInput(createInputCentered(mm2px(Vec(9.7, 56.3)), module, Eurorack_pmod::IN3_INPUT)); 134 | 135 | addOutput(createOutputCentered(mm2px(Vec(9.7, 70.0)), module, Eurorack_pmod::OUT0_OUTPUT)); 136 | addOutput(createOutputCentered(mm2px(Vec(9.7, 83.8)), module, Eurorack_pmod::OUT1_OUTPUT)); 137 | addOutput(createOutputCentered(mm2px(Vec(9.7, 97.4)), module, Eurorack_pmod::OUT2_OUTPUT)); 138 | addOutput(createOutputCentered(mm2px(Vec(9.7, 111.3)), module, Eurorack_pmod::OUT3_OUTPUT)); 139 | 140 | addChild(createRectLight(module, 5.7, 20.9+13.8*0, Eurorack_pmod::LED_IN0_LIGHT)); 141 | addChild(createRectLight(module, 5.7, 20.9+13.8*1, Eurorack_pmod::LED_IN1_LIGHT)); 142 | addChild(createRectLight(module, 5.7, 20.9+13.8*2, Eurorack_pmod::LED_IN2_LIGHT)); 143 | addChild(createRectLight(module, 5.7, 20.9+13.8*3, Eurorack_pmod::LED_IN3_LIGHT)); 144 | addChild(createRectLight(module, 5.7, 20.9+13.8*4, Eurorack_pmod::LED_OUT0_LIGHT)); 145 | addChild(createRectLight(module, 5.7, 20.9+13.8*5, Eurorack_pmod::LED_OUT1_LIGHT)); 146 | addChild(createRectLight(module, 5.7, 20.9+13.8*6, Eurorack_pmod::LED_OUT2_LIGHT)); 147 | addChild(createRectLight(module, 5.7, 20.9+13.8*7, Eurorack_pmod::LED_OUT3_LIGHT)); 148 | } 149 | }; 150 | 151 | 152 | Model* modelEurorack_pmod = createModel("eurorack-pmod"); 153 | -------------------------------------------------------------------------------- /src/plugin.cpp: -------------------------------------------------------------------------------- 1 | #include "plugin.hpp" 2 | 3 | extern Model* modelEurorack_pmod; 4 | 5 | 6 | Plugin* pluginInstance; 7 | 8 | 9 | void init(Plugin* p) { 10 | pluginInstance = p; 11 | 12 | // Add modules here 13 | // p->addModel(modelMyModule); 14 | 15 | // Any other plugin initialization may go here. 16 | // As an alternative, consider lazy-loading assets and lookup tables when your module is created to reduce startup times of Rack. 17 | 18 | p->addModel(modelEurorack_pmod); 19 | } 20 | -------------------------------------------------------------------------------- /src/plugin.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | 5 | using namespace rack; 6 | 7 | // Declare the Plugin, defined in plugin.cpp 8 | extern Plugin* pluginInstance; 9 | 10 | // Declare each Model, defined in each module source file 11 | // extern Model* modelMyModule; 12 | --------------------------------------------------------------------------------