Skip to content

Commit 2392dc4

Browse files
committed
Exercise 4
1 parent ce2a6ec commit 2392dc4

File tree

6 files changed

+350
-0
lines changed

6 files changed

+350
-0
lines changed

04_signs.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Exercise 4: Signs
2+
3+
## What you'll do, part 1:
4+
5+
Write a module that takes a 64-bit signed number, and negates it in three ways:
6+
7+
* Invert and add 1.
8+
* Directly negate it.
9+
* Starting from the least significant bit, copy up to and including the first 1, then invert the remaining bits.
10+
11+
That last one seems a bit strange, but it works like this (using 8-bit numbers):
12+
13+
Given as input `11101000`, copy up to and including the first 1 (`1000`), then invert the remaining bits: `00011000`. Compare this to inverting first (`00010111`) and adding one: `00011000`. Hey, at least the last technique doesn't require an adder.
14+
15+
Formally verify that:
16+
17+
* These three techniques always result in the same number.
18+
* The most significant bit of the number being set always means it is less than 0.
19+
20+
When you complete this exercise, think about how long the formal verification engine took to run. Formal verification is not a brute-force process. If it were, then even at one check per nanosecond, it would take over 500 years to verify every 64-bit number. Rather, the formal verification engine is a solver, technically an [SMT solver](https://en.wikipedia.org/wiki/Satisfiability_modulo_theories), and every built-in theory such a solver has enables the solver to shortcut some brute-force approaches. The solver we have configured to use in the `[engines]` section of the sby file is the [Z3 solver](https://en.wikipedia.org/wiki/Z3_Theorem_Prover), which has overall good performance.
21+
22+
### Signedness
23+
24+
Up until now, when you've created a Signal, you've created unsigned signals. This means that arithmetic and comparisons on them are unsigned. So, the 4-bit unsigned number `1000` is always *greater* than the 4-bit unsigned number `0111`.
25+
26+
However, in 2's complement, `1000` is -8 while `0111` is 7, which means that if these 4-bit signals were treated as *signed*, `1000` should be *less* than `0111`.
27+
28+
```python
29+
x = Signal(signed(4))
30+
y = Signal(signed(4))
31+
m.d.comb += [
32+
x.eq(0b1000),
33+
y.eq(0b0111),
34+
Assert(x < y),
35+
]
36+
```
37+
38+
To treat an unsigned (or signed) signal as signed:
39+
40+
```python
41+
x = Signal(4)
42+
y = Signal(4)
43+
m.d.comb += [
44+
x.eq(0b1000),
45+
y.eq(0b0111),
46+
Assert(x.as_signed() < y.as_signed()),
47+
]
48+
```
49+
50+
There is also a corresponding `as_unsigned()` function to convert a signed (or unsigned) signal to an unsigned signal.
51+
52+
Negating a signal always results in a signed signal, regardless of whether the input signal is signed or unsigned:
53+
54+
```python
55+
x = Signal(4)
56+
y = Signal(4)
57+
m.d.comb += [
58+
x.eq(0b0011),
59+
y.eq(0b1101),
60+
Assert(y > x),
61+
Assert(-x < x),
62+
]
63+
```
64+
65+
Equality, on the other hand, does not take into account signedness. It is bit-for-bit equality:
66+
67+
```python
68+
x = Signal(4)
69+
y = Signal(4)
70+
m.d.comb += [
71+
x.eq(0b0011),
72+
y.eq(0b1101),
73+
Assert(y > x),
74+
Assert(-x == y),
75+
]
76+
```
77+
78+
All arithmetic operations that apply to signed signals are signed. For example, right shifts do take into account signedness, and the result is the same signedness as the input:
79+
80+
```python
81+
x = Signal(4)
82+
y = Signal(signed(4))
83+
m.d.comb += [
84+
x.eq(0b1000),
85+
y.eq(0b1000),
86+
Assert((x >> 1) == 0b0100),
87+
Assert((y >> 1) == 0b1100),
88+
]
89+
```
90+
91+
### Library: priority encoder
92+
93+
A *priority encoder* is a circuit that takes an N-bit input and outputs the position of the first set bit, where "first" could mean most significant or least significant. nMigen has a coding library with a PriorityEncoder module built in, which outputs the position of the first least significant bit set.
94+
95+
```python
96+
from nmigen.lib.coding import PriorityEncoder
97+
98+
enc = PriorityEncoder(width=8)
99+
input = Signal(8)
100+
output = Signal(3)
101+
bit_set = Signal()
102+
103+
m.d.comb += [
104+
enc.i.eq(input),
105+
output.eq(enc.o),
106+
bit_set.eq(~enc.n), # n really means that the input is zero
107+
]
108+
```
109+
110+
For example, the input `0101000` will result in the output being 3 and bit_set being high.
111+
112+
### Limitations on slices
113+
114+
Slices such as `x[1:4]` require integers as indices, not signals. So you can't have:
115+
116+
```python
117+
x = Signal(8)
118+
y = Signal(3)
119+
z = x[y] # Results in an error
120+
```
121+
122+
You can achieve the same effect using bit tricks. For example, `1 << y` gives you a 1 in the yth bit position. `(x >> y) & 1` gives you the yth bit of x.
123+
124+
What does `(-1 << y) & x` give you?
125+
126+
-----
127+
128+
Interested in more bit-manipulation tricks? Check out Knuth's The Art of Computer Programming, Volume 4A, chapter 7.1.3 (Bitwise Tricks and Techniques). Hacker's Delight also contains copious bit manipulation fun.
129+
130+
-----
131+
132+
## What you'll do, part 2:
133+
134+
Suppose you have a chip that can compare two 16-bit numbers, but only as unsigned numbers. It outputs whether the first is less than the second. Write such a module.
135+
136+
Now write another module that uses the above module, and based only the most significant bits (i.e. the sign bits) of the original inputs, and the output of the unsigned comparator, outputs whether the first is less than the second, treated as *signed* numbers. The idea here is that you're limited to an unsigned comparator, and need a signed comparator.
137+
138+
Formally verify that the output of your module is correct, by using `as_signed()`.
139+
140+
## Stumped?
141+
142+
The answers are in [`answers/e04_negate.py`](answers/e04_negate.py) and [`answers/e04_signed_compare.py`](answers/e04_signed_compare.py)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ Work in progress!
66
* [01 - Counting coin: basic concepts](01_input.md)
77
* [02 - The day after: more if, and switch-case](02_switch.md)
88
* [03 - Life finds a way: parts of signals](03_parts.md)
9+
* [04 - Signs: signed signals](04_signs.md)

answers/e04_negate.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Disable pylint's "your name is too short" warning.
2+
# pylint: disable=C0103
3+
from typing import List, Tuple
4+
5+
from nmigen import Signal, Module, Elaboratable
6+
from nmigen.build import Platform
7+
from nmigen.asserts import Assume, Assert, Cover
8+
from nmigen.lib.coding import PriorityEncoder
9+
10+
from util import main
11+
12+
13+
class Negate(Elaboratable):
14+
"""Logic for the Negate module."""
15+
16+
def __init__(self):
17+
self.input = Signal(64)
18+
self.output1 = Signal(64) # invert and add 1
19+
self.output2 = Signal(64) # direct negation
20+
self.output3 = Signal(64) # using priority encoder
21+
22+
def elaborate(self, _: Platform) -> Module:
23+
"""Implements the logic for the Negate module."""
24+
m = Module()
25+
m.submodules.enc = enc = PriorityEncoder(64)
26+
27+
m.d.comb += self.output1.eq(~self.input + 1)
28+
m.d.comb += self.output2.eq(-self.input)
29+
m.d.comb += enc.i.eq(self.input)
30+
31+
with m.If(enc.n):
32+
m.d.comb += self.output3.eq(0)
33+
with m.Else():
34+
neg1 = Signal(64)
35+
m.d.comb += neg1.eq(-1)
36+
# n
37+
# 1111111111000000 # mask = -1 << n
38+
# 0000000000111111 # lower_mask = ~(-1 << n)
39+
# 0000000001000000 # 1 << n
40+
mask = Signal(64)
41+
m.d.comb += mask.eq(neg1 << enc.o)
42+
lower_mask = ~mask
43+
m.d.comb += self.output3.eq((~self.input & mask)
44+
| (self.input & lower_mask)
45+
| (1 << enc.o))
46+
47+
return m
48+
49+
@classmethod
50+
def formal(cls) -> Tuple[Module, List[Signal]]:
51+
"""Formal verification for the Negate module."""
52+
m = Module()
53+
m.submodules.c = c = cls()
54+
55+
m.d.comb += Assert(c.output1 == c.output2)
56+
m.d.comb += Assert(c.output2 == c.output3)
57+
m.d.comb += Assert(c.output1[63] == (c.output1.as_signed() < 0))
58+
59+
return m, [c.input]
60+
61+
62+
if __name__ == "__main__":
63+
main(Negate)

answers/e04_negate.sby

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[tasks]
2+
cover
3+
bmc
4+
5+
[options]
6+
bmc: mode bmc
7+
cover: mode cover
8+
depth 1
9+
multiclock on
10+
11+
[engines]
12+
cover: smtbmc z3
13+
bmc: smtbmc z3
14+
15+
[script]
16+
read_verilog <<END
17+
module \$dff (CLK, D, Q);
18+
parameter WIDTH = 0;
19+
parameter CLK_POLARITY = 1'b1;
20+
input CLK;
21+
input [WIDTH-1:0] D;
22+
output reg [WIDTH-1:0] Q;
23+
\$ff #(.WIDTH(WIDTH)) _TECHMAP_REPLACE_ (.D(D),.Q(Q));
24+
endmodule
25+
END
26+
design -stash dff2ff
27+
read_ilang toplevel.il
28+
proc
29+
techmap -map %dff2ff top/w:clk %co
30+
prep -top top
31+
32+
[files]
33+
toplevel.il

answers/e04_signed_compare.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Disable pylint's "your name is too short" warning.
2+
# pylint: disable=C0103
3+
from typing import List, Tuple
4+
5+
from nmigen import Signal, Module, Elaboratable
6+
from nmigen.build import Platform
7+
from nmigen.asserts import Assume, Assert, Cover
8+
from nmigen.lib.coding import PriorityEncoder
9+
10+
from util import main
11+
12+
13+
class UnsignedComparator(Elaboratable):
14+
"""Logic for the UnsignedComparator module."""
15+
16+
def __init__(self):
17+
self.a = Signal(16)
18+
self.b = Signal(16)
19+
self.lt = Signal()
20+
21+
def elaborate(self, _: Platform) -> Module:
22+
"""Implements the logic for the UnsignedComparator module."""
23+
m = Module()
24+
25+
m.d.comb += self.lt.eq(self.a < self.b)
26+
27+
return m
28+
29+
30+
class SignedComparator(Elaboratable):
31+
"""Logic for the SignedComparator module."""
32+
33+
def __init__(self):
34+
self.a = Signal(16)
35+
self.b = Signal(16)
36+
self.lt = Signal()
37+
38+
def elaborate(self, _: Platform) -> Module:
39+
"""Implements the logic for the SignedComparator module."""
40+
m = Module()
41+
m.submodules.ucmp = ucmp = UnsignedComparator()
42+
43+
ult = Signal() # Unsigned less than
44+
45+
# Hook up the submodule
46+
m.d.comb += [
47+
ucmp.a.eq(self.a),
48+
ucmp.b.eq(self.b),
49+
ult.eq(ucmp.lt),
50+
]
51+
52+
is_a_neg = self.a[15]
53+
is_b_neg = self.b[15]
54+
55+
with m.If(~is_a_neg & ~is_b_neg):
56+
m.d.comb += self.lt.eq(ult)
57+
with m.Elif(is_a_neg & ~is_b_neg):
58+
m.d.comb += self.lt.eq(1)
59+
with m.Elif(~is_a_neg & is_b_neg):
60+
m.d.comb += self.lt.eq(0)
61+
with m.Else():
62+
m.d.comb += self.lt.eq(ult)
63+
64+
return m
65+
66+
@classmethod
67+
def formal(cls) -> Tuple[Module, List[Signal]]:
68+
"""Formal verification for the SignedComparator module."""
69+
m = Module()
70+
m.submodules.c = c = cls()
71+
72+
m.d.comb += Assert(c.lt == (c.a.as_signed() < c.b.as_signed()))
73+
74+
return m, [c.a, c.b]
75+
76+
77+
if __name__ == "__main__":
78+
main(SignedComparator)

answers/e04_signed_compare.sby

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[tasks]
2+
cover
3+
bmc
4+
5+
[options]
6+
bmc: mode bmc
7+
cover: mode cover
8+
depth 1
9+
multiclock on
10+
11+
[engines]
12+
cover: smtbmc z3
13+
bmc: smtbmc z3
14+
15+
[script]
16+
read_verilog <<END
17+
module \$dff (CLK, D, Q);
18+
parameter WIDTH = 0;
19+
parameter CLK_POLARITY = 1'b1;
20+
input CLK;
21+
input [WIDTH-1:0] D;
22+
output reg [WIDTH-1:0] Q;
23+
\$ff #(.WIDTH(WIDTH)) _TECHMAP_REPLACE_ (.D(D),.Q(Q));
24+
endmodule
25+
END
26+
design -stash dff2ff
27+
read_ilang toplevel.il
28+
proc
29+
techmap -map %dff2ff top/w:clk %co
30+
prep -top top
31+
32+
[files]
33+
toplevel.il

0 commit comments

Comments
 (0)