diff --git a/sim/CMakeLists.txt b/sim/CMakeLists.txt index 850bdf2..5d652d8 100644 --- a/sim/CMakeLists.txt +++ b/sim/CMakeLists.txt @@ -41,6 +41,8 @@ target_sources(sim PRIVATE src/main.cpp ) +target_include_directories(sim PRIVATE inc/) + list(APPEND VSOURCES ../verilog/hub75e.sv ../verilog/lineram.v) diff --git a/sim/inc/devices.hpp b/sim/inc/devices.hpp new file mode 100644 index 0000000..5d3354e --- /dev/null +++ b/sim/inc/devices.hpp @@ -0,0 +1,150 @@ +// Project-specific cosimuluated devices. +#pragma once +#include "tests.hpp" +#include "Vhub75e.h" + +// slices the RGB values for us. +uint8_t rgb_slice(uint32_t rgb, uint8_t bit) { + if (bit > 8) { + // todo: panic + return 0; + } + uint8_t r = (rgb >> (16 + bit)) & 1; + uint8_t g = (rgb >> (8 + bit)) & 1; + uint8_t b = (rgb >> bit) & 1; + return (r << 2) & (g << 1) & (b << 1); +} + +void rgb_unslice(unsigned int &rgb, uint8_t bits, uint8_t bitpos) { + if (bitpos > 7 || bits > 0b111) { + // TODO: panic. + return; + } + auto r = (bits >> 2) & 1; + auto g = (bits >> 1) & 1; + auto b = (bits >> 0) & 1; + + rgb |= r << bitpos << 16; + rgb |= g << bitpos << 8; + rgb |= b << bitpos << 0; +} + +class HUB75Reciever : public CosimulatedDevice { + + typedef std::vector row_array; + int xsize; + int ysize; + + row_array row0{}; + row_array row1{}; + + // the previous row values that were latched in. + std::vector> past_rows{}; + // the pulse width for each output, in clock cycles. + std::vector pulse_widths{}; + + int bit_position = 7; // the bit that is currently being shifted in + + int output_period_cnt; + // if oe = 0, count clocks. when oe = 1, store value into + // pulse_widths[display_bit]; + + // previous latch value, used to identify when to latch. + unsigned char prev_latch = 0; + // previous display clock value, used to detect rising edge. + unsigned char prev_display_clk = 0; + + unsigned char prev_clk = 0; + + unsigned char prev_oe = 1; // assuming starting high. + + // references to the panel driver signals. + VL_IN8(&display_clk, 0, 0); + VL_IN8(&out_enable, 0, 0); + VL_IN8(&latch, 0, 0); + VL_IN8(&rgb0, 2, 0); + VL_IN8(&rgb1, 2, 0); + VL_IN8(&clk, 0, 0); + +public: + HUB75Reciever(int xsize, int ysize, const Vhub75e &dut) + : clk(dut.clk), display_clk(dut.display_clk), out_enable(dut.out_enable), + latch(dut.latch), rgb0(dut.panel_rgb0), rgb1(dut.panel_rgb1) { + this->xsize = xsize; + this->ysize = ysize; + row0.clear(); + prev_oe = out_enable; + prev_display_clk = display_clk; + prev_latch = latch; + prev_clk = clk; + }; + + // evaluates the reciever. + virtual void tick() override { + + if (prev_display_clk == 0 && display_clk == 1) { + // display clock rising edge. + row0.push_back(rgb0); + row1.push_back(rgb1); + } + + if (prev_latch == 0 && latch == 1) { + // latch in the data: reverse the rows, and pu + std::reverse(row0.begin(), row0.end()); + std::reverse(row1.begin(), row1.end()); + past_rows.push_back(std::pair(row0, row1)); + row0.clear(); + row1.clear(); + } + + if (prev_clk == 0 && clk == 1) { + if (out_enable == 0) { + if (prev_oe == 1) { + // falling edge. + output_period_cnt = 1; + } else { + output_period_cnt++; + } + } else { // out_enable == 1 + if (prev_oe == 1) { + // do nothing + } + if (prev_oe == 0) { + // rising edge + pulse_widths.push_back(output_period_cnt); + } + } + } + + // update previous values + prev_display_clk = display_clk; + prev_latch = latch; + prev_oe = out_enable; + prev_clk = clk; + } + + const auto &get_past_rows() { return this->past_rows; } + const std::vector &get_pulse_widths() { return this->pulse_widths; } + + // return the RGB version. + std::pair, std::vector> transpose() { + auto r0rgb = std::vector(xsize, 0); + auto r1rgb = std::vector(xsize, 0); + + auto bitdepth = pulse_widths.size(); + + // TODO: use more sophisticated slicing. + auto slice = bitdepth - 1; + for (const auto &[row0slice, row1slice] : this->past_rows) { + + for (int i = 0; i < row0slice.size(); i++) { + rgb_unslice(r0rgb[i], row0slice[i], slice); + rgb_unslice(r1rgb[i], row1slice[i], slice); + } + + slice--; + } + + return std::pair(r0rgb, r1rgb); + } +}; diff --git a/sim/inc/tests.hpp b/sim/inc/tests.hpp new file mode 100644 index 0000000..208962e --- /dev/null +++ b/sim/inc/tests.hpp @@ -0,0 +1,204 @@ +// Common Verilator Simulation/test constructs. +// Includes a simulation-fixture useful for writing tests. +#pragma once +#include "verilated.h" +#include "verilated_vcd_c.h" +#include +#include +#include +#include +#include + + +// represents a generic co-simulated device. +// While this abstract class is very simple, it enables dynamic behavior to be added to the +// simulation fixture. +class CosimulatedDevice { +public: + virtual ~CosimulatedDevice(){}; + + virtual void tick() = 0; +}; + + +// Simple stimulus class used to trigger basic operations +class PulseStimulus : public CosimulatedDevice { + unsigned long width; + unsigned char &signal; + + unsigned long counter = 0; + +public: + PulseStimulus(unsigned char &signal, unsigned long width) : signal(signal) { + this->width = width; + } + virtual void tick() override { + if (counter < width) { + signal = 1; + } else { + signal = 0; + } + counter++; + } +}; + +class FakeBRAM : public CosimulatedDevice { + std::array ram{}; + + std::queue addr_lookup_q; + + unsigned short &addr_in; + unsigned long &data_out; + + unsigned char &clk; + +public: + FakeBRAM(int latency, unsigned char &clk, unsigned short &addr_in, + unsigned long &data_out) + : addr_lookup_q(), addr_in(addr_in), data_out(data_out), clk(clk) { + for (int i = 0; i < latency; i++) { + addr_lookup_q.push(0); + } + + for (int i = 0; i < ram.size(); i++) { + ram[i] = i + 3; + } + }; + + FakeBRAM(int latency, unsigned char &clk, unsigned short &addr_in, + unsigned long &data_out, std::array ram) + : addr_lookup_q(), addr_in(addr_in), data_out(data_out), clk(clk), + ram(ram) { + for (int i = 0; i < latency; i++) { + addr_lookup_q.push(0); + } + } + + void tick() { + // we push and pop in the same tick: this way we keep the queue the same + // size, acting as a pipeline delay. + addr_lookup_q.push(addr_in); + + auto addr_to_load = addr_lookup_q.front(); + addr_lookup_q.pop(); + data_out = ram.at(addr_to_load); + } + + // TODO: allow accessing/setting the data + const std::span get() { return this->ram; } + + void write(unsigned short addr, unsigned long data) { ram[addr] = data; } +}; + + +// test fixture to reduce amount of runtime code. +// Supports: +// adding external modules +// running the test +// storing if the done flag was raised (or not) +// +// TODO: tracing. +template class VerilatorTestFixture { +public: + enum class FinishReason { Ok, Timeout }; + +private: + // we call .tick() on all of these. They are bound externally to the DUT. + std::vector> external_devices; + std::unique_ptr ctx; + std::unique_ptr dut; + + unsigned long timeout = + 1000000; // clock cycles to execute before ending the simulation. + unsigned long simtime = 0; + unsigned long posedge = 0; + + // stores the termination-condition for the simulation. + // if false, it means we timed out. If true, our done_condition returned true. + enum FinishReason reason; + + typedef std::function done_callback; + // callback function to determine execution completion. + // This can be used to add arbitrary finish-conditions to the execution. + // If the function returns true, we set the done_set boolean to true; + done_callback done_check; + + std::unique_ptr trace; + +public: + // Create a new test fixture with a given timeout. Everything else (including + // done-condition) can be set later. + VerilatorTestFixture() { + ctx = std::make_unique(); + dut = std::make_unique(ctx.get(), "dut"); + dut->eval(); // let values settle before adding modules. + } + + void set_timeout(unsigned long new_timeout) { this->timeout = new_timeout; } + + void set_done_callback(done_callback d) { this->done_check = d; } + + void add_module(std::shared_ptr device) { + external_devices.push_back(device); + } + + void enable_trace(std::string name) { + if (!trace) { + ctx->traceEverOn(true); + trace = std::make_unique(); + dut->trace(trace.get(), 99); + trace->open(name.c_str()); + } + } + + void exec() { + bool done = false; + dut->eval(); // pre-eval. + while (!done) { + dut->clk ^= 1; + dut->eval(); + + if (trace) { + trace->dump(10 * simtime); + } + + if (dut->clk == 1) { + posedge++; + } + if (done_check) { + if (done_check(*dut, posedge)) { + reason = FinishReason::Ok; + done = true; + } + } + + if (posedge >= timeout) { + reason = FinishReason::Timeout; + done = true; + } + // run our external devices + for (auto dev : external_devices) { + dev->tick(); + } + if (trace) { + trace->dump(10 * simtime + 5); + } + dut->eval(); // allow combinational logic to settle if it's being set on + // the negative clock. + if (trace) { + trace->dump(10 * simtime + 6); + } + simtime++; + } + + if (trace) { + // close the trace. + trace->close(); + } + } + + // return a reference to the DUT itself. Useful for bespoke tests. + const DUT &get() { return *dut; } + + const FinishReason get_reason() { return reason; } +}; diff --git a/sim/src/main.cpp b/sim/src/main.cpp index a55eb4a..be0bf13 100644 --- a/sim/src/main.cpp +++ b/sim/src/main.cpp @@ -1,163 +1,14 @@ #include "Vhub75e.h" +#include "tests.hpp" +#include "devices.hpp" #include "verilated.h" #include "verilated_vcd_c.h" #include #include -#include -#include -#include #include -#include #include #include -class CosimulatedDevice { -public: - virtual ~CosimulatedDevice(){}; - - virtual void tick() = 0; -}; - -// slices the RGB values for us. -uint8_t rgb_slice(uint32_t rgb, uint8_t bit) { - if (bit > 8) { - // todo: panic - return 0; - } - uint8_t r = (rgb >> (16 + bit)) & 1; - uint8_t g = (rgb >> (8 + bit)) & 1; - uint8_t b = (rgb >> bit) & 1; - return (r << 2) & (g << 1) & (b << 1); -} - -class HUB75Reciever : public CosimulatedDevice { - - typedef std::vector row_array; - int xsize; - int ysize; - - row_array row_upper{}; - row_array row_lower{}; - - // the previous row values that were latched in. - std::vector past_rows{}; - // the pulse width for each output, in clock cycles. - std::vector pulse_widths{}; - - int bit_position = 7; // the bit that is currently being shifted in - - int output_period_cnt; - // if oe = 0, count clocks. when oe = 1, store value into - // pulse_widths[display_bit]; - - // previous latch value, used to identify when to latch. - unsigned char prev_latch = 0; - // previous display clock value, used to detect rising edge. - unsigned char prev_display_clk = 0; - - unsigned char prev_oe = 0; - - // references to the panel driver signals. - VL_IN8(&display_clk, 0, 0); - VL_IN8(&out_enable, 0, 0); - VL_IN8(&latch, 0, 0); - VL_IN8(&rgb0, 2, 0); - VL_IN8(&rgb1, 2, 0); - -public: - HUB75Reciever(int xsize, int ysize, Vhub75e &dut) - : display_clk(dut.display_clk), out_enable(dut.out_enable), - latch(dut.latch), rgb0(dut.panel_rgb0), rgb1(dut.panel_rgb1) { - this->xsize = xsize; - this->ysize = ysize; - row_upper.clear(); - }; - - // evaluates the reciever. - virtual void tick() override { - - if (prev_display_clk == 0 && display_clk == 1) { - // display clock rising edge. - row_upper.push_back(rgb0); - row_lower.push_back(rgb1); - } - - if (prev_latch == 0 && latch == 1) { - // latch in the data: reverse the rows, and pu - std::reverse(row_upper.begin(), row_upper.end()); - past_rows.push_back(row_upper); - row_upper.clear(); - } - if (out_enable == 0) { - if (prev_oe == 1) { - // falling edge. - output_period_cnt = 0; - } else { - output_period_cnt++; - } - } else { - // rising edge: store the output - } - - // update previous values - prev_display_clk = display_clk; - prev_latch = latch; - prev_oe = out_enable; - } - - const std::vector &get_past_rows() { return this->past_rows; } - const std::vector &get_pulse_widths() { return this->pulse_widths; } - - // return the RGB version. - std::vector transpose() { - auto vec = std::vector(); - - for (auto bits : this->row_upper) { - } - - return vec; - } -}; - -class FakeBRAM : public CosimulatedDevice { - std::array ram{}; - - std::queue addr_lookup_q; - - unsigned short &addr_in; - unsigned long &data_out; - - unsigned char &clk; - -public: - FakeBRAM(int latency, unsigned char &clk, unsigned short &addr_in, - unsigned long &data_out) - : addr_lookup_q(), addr_in(addr_in), data_out(data_out), clk(clk) { - for (int i = 0; i < latency; i++) { - addr_lookup_q.push(0); - } - - for (int i = 0; i < ram.size(); i++) { - ram[i] = i; - } - }; - - void tick() { - // we push and pop in the same tick: this way we keep the queue the same - // size, acting as a pipeline delay. - addr_lookup_q.push(addr_in); - - auto addr_to_load = addr_lookup_q.front(); - addr_lookup_q.pop(); - data_out = ram.at(addr_to_load); - } - - // TODO: allow accessing/setting the data - std::array &dump() { return ram; } - - void write(unsigned short addr, unsigned long data) { ram[addr] = data; } -}; - void LineDriverTest(VerilatedContext &ctx) { // create the hub75e driver and run some basic tests @@ -206,82 +57,6 @@ void LineDriverTest(VerilatedContext &ctx) { // LineDriverTest(*ctx); // }; -// test fixture to reduce amount of runtime code. -// Supports: -// adding external modules -// running the test -// storing if the done flag was raised (or not) -// -// TODO: tracing. -template class VerilatorTestFixture { - // we call .tick() on all of these. They are bound externally to the DUT. - std::vector> external_devices; - std::unique_ptr ctx; - std::unique_ptr dut; - - unsigned long timeout = - 1000000; // clock cycles to execute before ending the simulation. - unsigned long simtime = 0; - unsigned long posedge = 0; - - // stores the termination-condition for the simulation. - // if false, it means we timed out. If true, our done_condition returned true. - bool done_condition = false; - - typedef std::function done_callback; - // callback function to determine execution completion. - // This can be used to add arbitrary finish-conditions to the execution. - // If the function returns true, we set the done_set boolean to true; - done_callback done_check; - -public: - // Create a new test fixture with a given timeout. Everything else (including - // done-condition) can be set later. - VerilatorTestFixture() { - ctx = std::make_unique(); - dut = std::make_unique(ctx.get(), "dut"); - } - - void set_timeout(unsigned long new_timeout) { this->timeout = new_timeout; } - - void set_done_callback(done_callback d) { this->done_check = d; } - - void add_module(std::shared_ptr device) { - external_devices.push_back(device); - } - - void exec() { - bool done = false; - while (!done) { - dut->clk ^= 1; - dut->eval(); - - if (dut->clk == 1) { - posedge++; - } - if (done_check) { - if (done_check(dut, posedge)) { - done_condition = true; - done = true; - } - } - - if (posedge >= timeout) { - done = true; - } - // run our external devices - for (auto dev : external_devices) { - dev->tick(); - } - dut->eval(); // allow combinational logic to settle if it's being set on the negative clock. - simtime++; - } - } - - // return a reference to the DUT itself. Useful for bespoke tests. - DUT &get() { return dut; } -}; - TEST_CASE("Hub75 Test") { auto ctx = std::make_unique(); // setup DUT @@ -328,9 +103,61 @@ TEST_CASE("Hub75 Test") { for (int i = 0; i < rows.size(); i++) { auto r = rows[i]; - using Catch::Matchers::SizeIs; - CHECK_THAT(r, SizeIs(128)); } } SECTION("Pulse width") {} } + +TEST_CASE("HUB75E Driver Test") { + auto fixture = VerilatorTestFixture(); + // very simple done checker. + auto done_check = [](Vhub75e &dut, unsigned long time) { + return dut.done == 1; + }; + fixture.set_done_callback(done_check); + const Vhub75e &dut = fixture.get(); + auto stim = std::make_shared(dut.write_trig, 4); + fixture.set_timeout(250000); + + fixture.add_module(stim); + + SECTION("Smoke Tests") { + fixture.enable_trace("testing.vcd"); + auto bram = std::make_shared(1, dut.clk, dut.pixbuf_addr, + dut.pixbuf_data); + fixture.add_module(bram); + auto display = std::make_shared(128, 64, dut); + fixture.add_module(display); + + fixture.exec(); + + CHECK(fixture.get_reason() == + VerilatorTestFixture::FinishReason::Ok); + + auto rows = display->get_past_rows(); + CHECK(rows.size() == 8); + for (int i = 0; i < rows.size(); i++) { + auto &[r0, r1] = rows[i]; + CHECK(r0.size() == 128); + CHECK(r1.size() == 128); + } + // pulse width smoke tests. + auto pulses = display->get_pulse_widths(); + REQUIRE(pulses.size() == rows.size()); + for (int i = 1; i < pulses.size(); i++) { + REQUIRE(pulses[i] == pulses[i - 1] / 2); + } + auto [row0, row1] = display->transpose(); + REQUIRE(row0.size() == 128); + REQUIRE(row1.size() == 128); + auto ram_ref = bram->get(); + + CAPTURE(row0); + CHECK(std::equal(ram_ref.begin(), ram_ref.begin() + 128, row0.begin(), + row0.end())); + } + SECTION("Line Correctness") { + // this is the part where we validate that the line in = line out. + // we have to generate different values since the + } +}