Skip to content

MAC Unit — Study Notes

Explains everything needed to understand this build from scratch: the algorithm, every chip, every supporting concept (debounce, decoupling, etc), and a fully worked example.


1. Why multiplication in hardware = shift and add

Digital logic has no native "multiply" operation — only combinations of AND, OR, NOT gates, which naturally give you addition (that's what an adder circuit is built from) and shifting (moving bits left/right through registers).

Long multiplication by hand in decimal: 23 x 14 = (23 x 4) + (23 x 10, shifted one place left), then add the two partial results.

Binary works the same way, except each digit of the multiplier is only 0 or 1. So instead of "multiply by each digit," it becomes: for each bit of the multiplier, either add the multiplicand (if that bit is 1) or add nothing (if it's 0) — then shift to align with the next position. Do this once per bit of the multiplier, and the running total is the final product.

For 4-bit x 4-bit, that's 4 cycles, one per bit of the multiplier.

2. The three registers and what each physically holds

M (multiplicand) — loaded once at the start, held steady the whole time. Feeds into the adder every cycle without changing.

Q (multiplier) — shifts right by one bit every cycle. Its rightmost bit (LSB) is checked each cycle to decide "add or don't add." After shifting, that bit is gone and the next bit takes its place at the LSB position.

A (partial product accumulator, not to be confused with the final 12-bit accumulator in section 3) — this is the part that's easy to misunderstand. A is not separate from Q — together {A, Q} form one combined 8-bit register that shifts right as a single unit every cycle. Reasoning: as bits shift down from A into Q's top, and Q's already-used bottom bits fall off the far end, the product gradually builds up across the combined register. After 4 cycles, A holds the upper 4 bits of the final product and Q (having shifted 4 times) holds the lower 4 bits.

3. Why there's a carry bit "C" in {C, A, Q}

Adding two 4-bit numbers (A + M) can overflow into a 5th bit — max case 15 + 15 = 30, which needs 5 bits to represent. That overflow is the adder's carry-out. If it's thrown away, information is silently lost every time an addition overflows. So it's captured as "C" and shifted in as part of the combined {C, A, Q} 9-bit right-shift each cycle, then becomes part of A's new top bit after the shift. This is why the datapath is described as 9 bits wide, not 8.

4. Each chip, what it does, and why it's the right part

74LS175 (used for M register, and separately for A register, and again

for the accumulator's upper 4 bits — 3 total in the design) A chip containing 4 D-type flip-flops that all load a new value on a clock edge and hold it steady until the next edge. This is exactly "load once, stay steady" — correct for M (loaded once, never changes during a multiply) and for A (needs to hold its value between the mux output and the next shift). Has a clear/reset pin, useful for resetting between test runs.

74LS194 (Q register)

A "universal shift register" — can shift left, shift right, parallel-load, or hold, selected by two mode-control pins. Used here in shift-right mode. Its serial-in pin (for shift-right mode) is where the incoming bit from A's LSB enters each cycle; its own LSB is what's tested for "add or don't add."

74LS283 (adder — 1 for the multiply stage, 3 chained for the 12-bit

accumulator stage, 4 total) Pure combinational 4-bit adder — no clock involved. It continuously reflects whatever's on its inputs, like a calculator display updating live, not requiring an "enter" button. Takes two 4-bit numbers, produces a 4-bit sum and 1-bit carry-out. Chaining three of these (carry-out of one feeding carry-in of the next) extends it to a 12-bit adder for the accumulator stage, since 3 x 4 bits = 12 bits.

74LS157 (mux)

A multiplexer: a selector switch built from gates. Has two sets of 4-bit inputs and one 4-bit output; a single select pin decides which input set passes through. Here, it implements "add or don't add": the adder always computes A+M in the background regardless of Q's LSB (since it's just combinational logic reacting to whatever's present), and the mux decides whether that sum is used (Q's LSB = 1) or discarded in favor of the unmodified A (Q's LSB = 0). This avoids needing separate gating logic on the adder itself.

74LS08 (AND gate, only 1 of its 4 gates used)

The adder's carry-out is only meaningful if an add actually happened. If Q's LSB was 0 (no add), any carry-out value from the adder is irrelevant and must be forced to 0 before being shifted into the chain — otherwise it corrupts the result. This gate takes (adder's real carry-out) AND (Q's LSB) as its two inputs: output is 1 only if both are true — meaning "an add happened, and it produced a real carry." This is an easy detail to miss and causes subtly wrong answers (off-by-some-bits) if skipped.

74LS164 (ring counter / sequencer)

An 8-bit serial-in shift register, wired here as a ring counter — meaning a single "1" bit is seeded in and walks through successive output positions one at a time, one per clock pulse, like a single lit dot moving down a row of LEDs. Tapping 4 of its outputs tells you which of the 4 multiply cycles you're currently on; the point where it would wrap back to the start becomes your "multiply is done, stop the clock" signal.

Alternative considered: a 74LS163 binary counter would use fewer chips, but requires extra decode logic to detect "count == 4" for the stop condition. The ring counter makes "which cycle, and are we done" directly visible on LEDs (one lit at a time, walking across 4 positions) — much easier to debug by eye against hand-computed cycle-by-cycle values, which is why it was chosen over the binary-counter alternative for this build.

74LS273 (accumulator register, lower 8 bits)

Similar role to 74LS175 (D flip-flops, load-and-hold on clock edge) but with 8 bits and a dedicated active-low clear pin — convenient for resetting the accumulator to zero between full runs.

NE555 (clock)

Configured in astable mode, this classic timer chip free-runs, continuously producing a square wave with no external trigger needed — this is the system's clock, the periodic signal every register waits for before updating. Timing (frequency) is set by external resistor/capacitor values. Slow it down (1-2 Hz) during debugging so you can watch each cycle on LEDs; speed it up once verified.

74LS14 (hex Schmitt-trigger inverter, used for debounce)

"Schmitt-trigger" means the chip has built-in hysteresis: it only flips its output when the input crosses a clearly-high or clearly-low threshold, ignoring noisy in-between wobble. Needed because mechanical pushbuttons physically bounce — make and break contact several times within milliseconds of being pressed. Without cleanup, one press looks like 5-10 rapid presses to downstream logic, corrupting a manual step count. One section of this chip cleans up the manual step button; other sections are unused here (comes with 6 inverters per chip).

5. Why decoupling capacitors matter (0.1uF ceramic, near every 2-3 chips)

Every chip draws a brief spike of current at the moment it switches state. Breadboard power/ground rails have small but real resistance and inductance along their length. A current spike from one chip switching can cause a brief voltage dip on a neighboring chip's supply pins, which can be misinterpreted as a false logic level or false clock edge. A capacitor placed physically close to a chip acts as a local charge reservoir that absorbs that spike before it propagates along the rail. This is the single most common fix for "circuit works sometimes, glitches randomly for no clear reason" on breadboard TTL builds — treat it as mandatory, not optional.

6. Worked example: 1010 x 0101 (10 x 5 = 50), full cycle by cycle

M = 1010, Q = 0101, A = 0000, C = 0 (starting state)

Cycle 1: Q's LSB (rightmost bit of 0101) = 1 -> add. A + M = 0000 + 1010 = 1010, carry C = 0. Shift {C,A,Q} = {0,1010,0101} right by 1 -> new A = 0101, new Q = 0010 (old Q's LSB 1 fell off; A's LSB moved into Q's top)

Cycle 2: Q's LSB (of 0010) = 0 -> don't add. A stays 0101 unchanged (mux passes A through). C = 0. Shift -> A = 0010, Q = 1001

Cycle 3: Q's LSB (of 1001) = 1 -> add. A + M = 0010 + 1010 = 1100, carry C = 0. Shift -> A = 0110, Q = 0100

Cycle 4: Q's LSB (of 0100) = 0 -> don't add. A stays 0110. Shift -> A = 0011, Q = 0010

Final {A,Q} = 00110010 binary = 50 decimal. Correct.

Use this exact table when debugging your real circuit — probe A and Q after each cycle and compare against these values line by line. The first cycle where your circuit's values diverge from this table tells you exactly which stage (adder, mux, carry gating, or the shift itself) is wired wrong.

7. What "MAC" actually means here, and its real limits

MAC = Multiply-ACCUMULATE: multiply two numbers, then add the result into a running total, repeatedly. A multiplier alone (sections 1-6) computes one product and stops — it's not a MAC until its output feeds an accumulator that keeps a running sum across multiple operations (section 8 below covers the accumulator stage itself).

What this circuit can do: compute a dot product of two arrays by hand — key in one (a,b) pair via switches, trigger a multiply, watch the result add into the 12-bit accumulator, then key in the next pair, repeat. After N pairs, the accumulator holds sum(a_i * b_i) for i = 1..N — exactly a dot product.

What it cannot do: - Hold an array in memory — there is no storage for a full array, only whatever's currently on the switches. You are the array — you feed it one element pair at a time by hand. (A memory + auto-walk phase using EEPROM + address counter + comparator was scoped and then deliberately cut for this build due to cost and complexity — see plan.md section 8.) - Operate in parallel — a real hardware accelerator (e.g. a systolic array) has many MAC units, each doing its own multiply-accumulate simultaneously in a single clock cycle, passing partial sums to neighboring cells automatically. This build does exactly one MAC at a time, sequentially, over multiple manual triggers. This is the honest distinction to state when describing the project publicly: "single MAC unit, sequentially operated" — not a parallel accelerator.

This single-unit build is the direct teaching bridge to a future systolic array project (many of these units wired together, each running in parallel) — understanding exactly what one unit does and doesn't do here is what makes "why wire N of these together" make real sense later, instead of being a hand-waved jump.

8. Accumulator stage — how the 12-bit running total works

After a multiply finishes, {A,Q} together hold the 8-bit product. This gets added into a 12-bit running total using three chained 74LS283 adders (see section 4) — computing (old accumulator value) + (new 8-bit product, zero-extended to 12 bits). The result loads into the accumulator registers (74LS273 for lower 8 bits, 74LS175 for upper 4 bits), replacing the old total. Next MAC operation repeats this against the new total, and so on.

12 bits was chosen because: a 4-term dot product's worst case is 4 x 15 x 15 = 900, which needs at least 10 bits (2^10 = 1024) to represent without overflow. 12 bits gives headroom above that while aligning cleanly with available chip widths (8-bit + 4-bit register combination), rather than needing an awkward 10-bit-wide register setup.

9. Quick reference — reading the ring counter during debugging

The ring counter's single walking "1" bit tells you which of the 4 multiply cycles is currently active. With 4 tapped LEDs (one per cycle position), you should see exactly one LED lit at any given time, moving to the next position on each clock pulse, cycling through all 4 positions once per multiply, then stopping (or wrapping, depending on wiring) once the multiply completes. If more than one LED is ever lit at once, or the position doesn't match which cycle you expect from the worked example in section 6, that's your first sign of a wiring or timing fault — check this before diving into the adder/mux logic.