|
| 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) |
0 commit comments