-
Parts and the simulation thereof
03/27/2021 at 08:32 • 0 commentsOne 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.
---------- more ----------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 beingawait
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.
-
Multi-value logic
03/24/2021 at 13:01 • 0 commentsWhen modelling hardware you run into the subject of multi-value logic. Inevitably, because digital logic is more than just ones and zeroes: there's also high impedance signals and their interactions with other signals to model.
Thus I had to settle upon a multi-value logic system for hdlpy.
---------- more ----------VHDL
I'm most familiar with VHDL and its 9-valued logic system from IEEE 1164:
U
Uninitialised X
Strong unknown 0
Strong logical zero 1
Strong logical one Z
High impedance W
Weak unknown L
Weak logical zero H
Weak logical one -
Don't care The main advantage of VHDL's system being that it allows you to model wired logic by using weak signals (
W
,L
andH
). However, hdlpy is intended for FPGA's, which typically don't do any wired logic (even high impedance signals are rare, only appearing on external pins).Verilog
Verilog sticks to the much simpler 4-value logic system as defined in IEEE 1364:
0
Logical zero 1
Logical one Z
High impedance X
Unknown Though missing weak signals, it does include high impedance signals which may appear on external FPGA pins.
hdlpy
Thus for hdlpy I settled on the Verilog system of 4-valued logic (
0
,1
,Z
andX
).Which then led to the challenge of deriving logical operations on these values. Of course,
0
and1
are easy:>>> from hdlpy import logic >>> logic(1) & logic(0) <logic.zero: '0'> >>> logic(1) & logic(1) <logic.one: '1'> >>> logic(1) | logic(0) <logic.one: '1'>
But what if one of the operands is
Z
orX
?I realised that a logical AND where one of the operands is
0
can only ever result in0
, irregardless of the second operand. The same goes for OR where one of the operands is1
:>>> logic(0) & logic('Z') <logic.zero: '0'> >>> logic(1) & logic('X') <logic.unknown: 'X'> >>> logic(0) | logic('Z') <logic.unknown: 'X'> >>> logic(1) | logic('X') <logic.one: '1'>
Indeed, VHDL and Verilog both define similar truth tables.
Use the source!
Source code for hdlpy, together with a test suite, is available on GitHub.