One of the more interesting things to do with a hardware definition language is of course the simulation of logic. Due to hefty build times when targeting an FPGA, simulation is the major way to develop and refine HDL projects. So of course hdlpy has to have a simulator.
Parts and signals
First I needed a way for hdlpy to know which logic signals belong together. VHDL calls this an entity, Verilog a module; hdlpy will call this a part. Of course, the bundling of code and logic signals readily maps onto a Python class, so that's what I'll use.
As an example, this is how you’d specify the signals of a simple flipflop in hdlpy:
@part
class Flipflop:
clk: logic # clock input
en: logic # input enable
d: logic # data input
o: logic # positive output
no: logic # negative output
Blocks
Blocks to execute whenever a certain triggering condition is met can be specified using @when
, like so:
@part
class Flipflop:
# ...
@when(rising = (‘clk’, ‘rst’))
def sync(self):
if self.rst:
self.o = 0
elif self.clk and self.en:
self.o = self.d
Blocks that are executed whenever any of their input signals change, used to simulate asynchronous logic, can be specified using @always
:
@part
class Flipflop:
# ...
@always
def async(self):
self.no = ~self.o
Simulation
All this is fairly straightforward. Simulation then becomes the far more difficult task of deciding when to execute these blocks and actually executing them.
I ended up making use of Python's coroutine support. A coroutine is basically a generator that yield
s objects being await
ed upon. Every block then becomes an infinite loop of first waiting for its triggers (or for a signal to change) and then executing the actual block. By implementing my own set of awaitable objects as well as an executor to handle them, I then had control over when blocks were executed.
As always, the code is on GitHub for those that are interested.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.