Introduction
On this page, we will present a stopwatch design. It is similar to the design in the Xilinx ISE tutorial. We will tackle it "the MyHDL way" and take it from spec to implementation.
This is an extensive example, and we will use it to present all aspects of a MyHDL-based design flow. It's also a relatively advanced. If you have difficulties understanding the material on this page, consider reading the first chapters of the manual or the earlier examples in this Cookbook first.
Specification
Compared to the design in the Xilinx ISE tutorial, our design is somewhat simplified. The intention is not to avoid complexity, but merely to make the code and the explanations better fit on a single web page. In particular, our stopwatch will only have three digits: two digits for the seconds, and one for the tenths of a second. Also, we will not consider clock generation issues and simply assume that a 10Hz clock is available.
The interface of the stopwatch design looks as follows:
@block def StopWatch(tens_led, ones_led, tenths_led, startstop, reset, clock): """ 3 digit stopwatch with seconds and tenths of a second. tens_led: 7 segment led for most significant digit of the seconds ones_led: 7 segment led for least significant digit of the seconds tenths_led: 7 segment led for tenths of a second startstop: input that starts or stops the stopwatch on a posedge reset: reset input clock: 10Hz clock input """
Architecture
A stopwatch system is naturally partitioned as follows:
- a subsystem that counts time, expressed as digits in bcd (binary coded decimal) code
- a subsystem that displays the count, by converting each bcd digit to a 7 segment led display
A natural partitioning often works best, and that's how we will approach the design. We will first design a time counter and then a bcd to led convertor.
Time counter design
Approach
One of the goals of the MyHDL project is to promote the use of modern software development techniques for hardware design. One such technique is the concept of unit testing, a cornerstone of extreme programming (XP).
Unit testing means writing a dedicated test for each building block of a design, and aggregating all tests in a regression test suite using a unit test framework. Moreover, the XP idea is to write the unit test first, before the actual implementation. This makes sure that the test writer concentrates on all aspects of the high level specification, without being influenced by lower level implementation details.
At the start of an implementation, the existing unit test will fail, and it will continue to do so until a valid implementation is achieved. The unit test thus serves as a metric for completion. Moreover, to see the unit test fail on incomplete or invalid designs enhances the confidence in the test quality itself. This is of crucial importance when making design changes later on.
Unit test
To write a unit test for building block, we need two things: the specification and the interface. The specification was described in previous sections. The interface of the time counter looks as follows:
from myhdl import * @block def TimeCount(tens, ones, tenths, startstop, reset, clock): """ 3 digit time counter in seconds and tenths of a second. tens: most significant digit of the seconds ones: least significant digit of the seconds tenths: tenths of a second startstop: input that starts or stops the counter on a posedge reset: reset input clock: 10Hz clock input """
The actual implementation is left open for now. We will first write the test, using the interface.
The following code is the unit test for the time counter subsystem in file test_TimeCount.py
:
from random import randrange from myhdl import * from TimeCount import TimeCount LOW, HIGH = bool(0), bool(1) MAX_COUNT = 6 * 10 * 10 PERIOD = 10 @block def bench(): """ Unit test for time counter. """ tens, ones, tenths = [Signal(intbv(0)[4:]) for i in range(3)] startstop, reset, clock = [Signal(LOW) for i in range(3)] dut = TimeCount(tens, ones, tenths, startstop, reset, clock) count = Signal(0) counting = Signal(False) @always(delay(PERIOD//2)) def clkgen(): clock.next = not clock @always(startstop.posedge, reset.posedge) def action(): if reset: counting.next = False count.next = 0 else: counting.next = not counting @always(clock.posedge) def counter(): if counting: count.next = (count + 1) % MAX_COUNT @always(clock.negedge) def monitor(): assert ((tens*100) + (ones*10) + tenths) == count @instance def stimulus(): for maxInterval in (100*PERIOD, 2*MAX_COUNT*PERIOD): for sig in (reset, startstop, reset, startstop, startstop, reset, startstop, startstop, startstop, reset, startstop, reset, startstop, startstop, startstop): yield delay(randrange(10*PERIOD, maxInterval)) yield clock.negedge # sync to avoid race condition sig.next = HIGH yield delay(100) sig.next = LOW raise StopSimulation return dut, clkgen, action, counter, monitor, stimulus def test_bench(): sim = Simulation(bench()) sim.run()
dut
is the design under test. clkgen
is a clock generator. action
defines
the stopwatch state, based on a rising edge on either of the input signals
startstop
or reset
. counter
maintains the expected time count. monitor
is the actual test: it asserts that the actual time count from the design
equals the expected time count. Finally, stimulus
defines a number of test
cases for the stopwatch. Note that it has an inner for
loop over signals, as
a concise way to define test patterns. This is straightforward in Python. But
think for a moment on how you would do it in Verilog or VHDL.
Also in stimulus
, note the yield clock.negedge
statement. This statement
synchronizes signal changes with the falling clock edge. This is needed to
avoid race conditions when signals change "simultaneously" with the rising
clock edge. This is commonly done in digital tests. As you can expect, this
statement was not present in the first version of the test: it was added after
the test was run against the implementation and found to fail occasionally,
even when the implementation was believed to be correct. This shows that in
practice there may be a good reason why a test needs to be adapted to get
everything working. But it in any case it is better to start with a "general"
unit test that is not influenced by an implementation.
Our unit test is now ready to run. We could actually run it directly against an
implementation. However, we will use it via the unit testing framework
py.test
instead. The framework provides the following functionality:
- it redefines the Python
assert
statement for extensive error reporting - it looks up and runs each method whose name starts with "test_"
- it looks up test modules by searching for modules whose name starts with "test_"
There's a lot more to say about py.test
and you are probably also curious
where to get it from. You can find that info further on this page, in the
section More about py.test.
Design
The following is an implementation of the time counter, in file TimeCount.py
:
from myhdl import * @block def TimeCount(tens, ones, tenths, startstop, reset, clock): """ 3 digit time counter in seconds and tenths of a second. tens: most significant digit of the seconds ones: least significant digit of the seconds tenths: tenths of a second startstop: input that starts or stops the counter on a posedge reset: reset input clock: 10kHz clock input """ @instance def logic(): seen = False counting = False while True: yield clock.posedge, reset.posedge if reset: tens.next = 0 ones.next = 0 tenths.next = 0 seen = False counting = False else: if startstop and not seen: seen = True counting = not counting elif not startstop: seen = False if counting: if tenths == 9: tenths.next = 0 if ones == 9: ones.next = 0 if tens == 5: tens.next = 0 else: tens.next = tens + 1 else: ones.next = ones + 1 else: tenths.next = tenths + 1 return logic
py.test
confirms that this is a valid implementation:
$ py.test test_TimeCount.py ============================= test session starts ============================== platform linux -- Python 3.7.3, pytest-7.1.1, pluggy-1.0.0 rootdir: /.../testing-examples-code/stopwatch collected 1 item test_TimeCount.py . [100%] ============================== 1 passed in 0.68s ===============================
bcd to led convertor design
Approach
For the design of the bcd to led convertor , we will follow a similar approach as before. We will write a unit test first, and then use it to complete the design.
We first put the encoding data in a separate module, seven_segment.py
, to
make it reusable. The appropriate data structure for the encoding is a
dictionary:
# 7 segment encoding # 0 # --- # 5 | | 1 # --- <- 6 # 4 | | 2 # --- # 3 encoding = {0: "1000000", 1: "1111001", 2: "0100100", 3: "0110000", 4: "0011001", 5: "0010010", 6: "0000010", 7: "1111000", 8: "0000000", 9: "0010000" }
Unit test
This is the unit test, in test_bcd2led.py
:
from random import randrange import seven_segment from myhdl import * from bcd2led import bcd2led PERIOD = 10 @block def bench(): led = Signal(intbv(0)[7:]) bcd = Signal(intbv(0)[4:]) clock = Signal(bool(0)) dut = bcd2led(led, bcd, clock) @always(delay(PERIOD//2)) def clkgen(): clock.next = not clock @instance def check(): for i in range(100): bcd.next = randrange(10) yield clock.posedge yield clock.negedge expected = int(seven_segment.encoding[int(bcd)], 2) assert led == expected raise StopSimulation return dut, clkgen, check def test_bench(): sim = Simulation(bench()) sim.run()
This test asserts that the led output from the design matches the appropriate encoding for a digit.
Design
Here is an implementation, in bcd2led.py
:
import seven_segment from myhdl import * code = [None] * 10 for key, val in seven_segment.encoding.items(): if 0 <= key <= 9: code[key] = int(val, 2) code = tuple(code) @block def bcd2led(led, bcd, clock): """ bcd to seven segment led convertor. led: seven segment led output bcd: bcd input clock: clock input """ @always(clock.posedge) def logic(): led.next = code[int(bcd)] return logic
Note how we derive the tuple code
from the encoding
dictionary. We need a
tuple because that's the data structure that the Verilog convertor supports.
It maps tuple indexing to a case statement to support ROM inferencing by
synthesis tools.
When we run py.test
, we get the following output:
$ py.test ============================= test process starts ============================== platform linux -- Python 3.7.3, pytest-7.1.1, pluggy-1.0.0 rootdir: /.../testing-examples-code/stopwatch collected 2 items test_TimeCount.py . [ 50%] test_bcd2led.py . [100%] ============================== 2 passed in 0.53s ===============================
Note that when run with no arguments, py.test
finds and runs all test
modules. This is done recursively through all subdirectories, making it
straightforward to run a full regression test suite.
Top level design
The top-level design in StopWatch.py
is just an assembly of the previously
designed modules:
from myhdl import * from TimeCount import TimeCount from bcd2led import bcd2led @block def StopWatch(tens_led, ones_led, tenths_led, startstop, reset, clock): """ 3 digit stopwatch with seconds and tenths of a second. tens_led: 7 segment led for most significant digit of the seconds ones_led: 7 segment led for least significant digit of the seconds tenths_led: 7 segment led for tenths of a second startstop: input that starts or stops the stopwatch on a posedge reset: reset input clock: 10Hz clock input """ tens, ones, tenths = [Signal(intbv(0)[4:]) for i in range(3)] timecount_inst = TimeCount(tens, ones, tenths, startstop, reset, clock) bcd2led_tens = bcd2led(tens_led, tens, clock) bcd2led_ones = bcd2led(ones_led, ones, clock) bcd2led_tenths = bcd2led(tenths_led, tenths, clock) return timecount_inst, bcd2led_tens, bcd2led_ones, bcd2led_tenths
Implementation
Automatic conversion to Verilog or VHDL
To go to an implementation, we first convert the design to Verilog
or VHDL automatically, using MyHDL's convert
function:
def convert(): tens_led, ones_led, tenths_led = [Signal(intbv(0)[7:]) for i in range(3)] startstop, reset, clock = [Signal(bool(0)) for i in range(3)] convInst = StopWatch(tens_led, ones_led, tenths_led, startstop, reset, clock) convInst.convert(hdl='Verilog') convInst.convert(hdl='VHDL') convert()
The resulting Verilog code is included in full:
module StopWatch ( tens_led, ones_led, tenths_led, startstop, reset, clock ); output [6:0] tens_led; reg [6:0] tens_led; output [6:0] ones_led; reg [6:0] ones_led; output [6:0] tenths_led; reg [6:0] tenths_led; input startstop; input reset; input clock; reg [3:0] ones; reg [3:0] tens; reg [3:0] tenths; always @(posedge clock, posedge reset) begin: STOPWATCH_TIMECOUNT0_LOGIC reg seen; reg counting; if (reset) begin tens <= 0; ones <= 0; tenths <= 0; seen = 1'b0; counting = 1'b0; end else begin if ((startstop && (!seen))) begin seen = 1'b1; counting = (!counting); end else if ((!startstop)) begin seen = 1'b0; end if (counting) begin if ((tenths == 9)) begin tenths <= 0; if ((ones == 9)) begin ones <= 0; if ((tens == 5)) begin tens <= 0; end else begin tens <= (tens + 1); end end else begin ones <= (ones + 1); end end else begin tenths <= (tenths + 1); end end end end always @(posedge clock) begin: STOPWATCH_BCD2LED0_LOGIC case (tens) 0: tens_led <= 64; 1: tens_led <= 121; 2: tens_led <= 36; 3: tens_led <= 48; 4: tens_led <= 25; 5: tens_led <= 18; 6: tens_led <= 2; 7: tens_led <= 120; 8: tens_led <= 0; default: tens_led <= 16; endcase end always @(posedge clock) begin: STOPWATCH_BCD2LED1_LOGIC case (ones) 0: ones_led <= 64; 1: ones_led <= 121; 2: ones_led <= 36; 3: ones_led <= 48; 4: ones_led <= 25; 5: ones_led <= 18; 6: ones_led <= 2; 7: ones_led <= 120; 8: ones_led <= 0; default: ones_led <= 16; endcase end always @(posedge clock) begin: STOPWATCH_BCD2LED2_LOGIC case (tenths) 0: tenths_led <= 64; 1: tenths_led <= 121; 2: tenths_led <= 36; 3: tenths_led <= 48; 4: tenths_led <= 25; 5: tenths_led <= 18; 6: tenths_led <= 2; 7: tenths_led <= 120; 8: tenths_led <= 0; default: tenths_led <= 16; endcase end endmodule
Note how the Verilog convertor expands the hierarchical design into a "flat net list of always blocks". The Verilog ouput is really an intermediate step towards an implementation. The whole design is flat and contained in a single file, which may make it easier to hand it off to back-end synthesis and implementation tools.
Note also how the convertor expands tuple indexing in MyHDL into a case statement in Verilog.
Synthesis
We will synthesize the design with Xilinx ISE 8.1. We first create a project in the ISE environment, add the source of the Verilog file to it, and we are ready to go.
The following is extracted from the synthesis report. It shows how the synthesis tool recognizes higher-level functions such as ROMs and counters:
========================================================================= * HDL Synthesis * ========================================================================= Synthesizing Unit <StopWatch>. Related source file is "/home/jand/dev/myhdl/example/cookbook/stopwatch/StopWatch.v". Found 16x7-bit ROM for signal <$n0007> created at line 68. Found 16x7-bit ROM for signal <$n0008> created at line 84. Found 16x7-bit ROM for signal <$n0009> created at line 100. Found 7-bit register for signal <tenths_led>. Found 7-bit register for signal <ones_led>. Found 7-bit register for signal <tens_led>. Found 1-bit register for signal <_StopWatch_timecount_inst_logic/counting>. Found 1-bit register for signal <_StopWatch_timecount_inst_logic/seen>. Found 4-bit up counter for signal <ones>. Found 4-bit up counter for signal <tens>. Found 4-bit up counter for signal <tenths>. Summary: inferred 3 ROM(s). inferred 3 Counter(s). inferred 23 D-type flip-flop(s). Unit <StopWatch> synthesized.
How these blocks are actually implemented depends on the target technology and the capabilities of the synthesis tool.
You can review the full FPGA synthesis report here.
FPGA implementation
The FPGA implementation report can be reviewed here.
CPLD implementation
The same design was also targetted to a CPLD technology. The detailed report can be viewed here.
More about py.test
To verify the stopwatch design, we have been using py.test
. However, this is
not the only unit testing framework available for Python. In fact, the standard
unit testing framework that comes with Python is the unittest
module. The
unittest
framework is presented in the MyHDL manual, and is used to verify
MyHDL itself. On the other hand, py.test
is not part of the standard Python
library currently. Why then did we use py.test
in this case?
The reason is that I believe that py.test
will be a better option in the
future. As demonstrated on this page, py.test
is non-intrusive. The only
thing we need to do for basic usage is to obey some simple naming conventions
and to use the assert
statement for testing - things we might want to do
without a testing framework anyway. In contrast, unittest
requires us to
wrap our tests into dedicated subclasses and to use special test methods. This
can be especially awkward with MyHDL, because MyHDL hardware is typically
described using top-level and embedded functions, not classes and methods.
In short, it is much easier to develop unit tests with py.test
than it is
with unittest
, in particular in the case of MyHDL code. However, py.test
also has its disadvantages:
- As
py.test
is not part of the standard Python library, it has to be installed separately. py.test
is currently not distributed in a convential way such as a tar file. It is part of thepy.lib
library that has to be checked out from a subversion repository. This requires the installation of a subversion client.- The use of the
assert
statement for unit testing is controversial in Python. Theassert
statement is originally intended for programmer usage, to make programs safer. However, in my opinion the use ofassert
for testing is natural and warranted. py.test
uses a lot of "magic" behind the scenes to modify Python's behavior for its purposes, such as extensive error reporting.
However, I believe that the benefits are far more important than the
disadvantages. Moreover, some disadvantages may disappear over time.
Consequently, I plan to promote py.test
as the unit testing framework of
choice for MyHDL in the future.
More info on the usage and installation of py.test
can be found
here.