You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
The previous lesson assembled logic gates into Boolean expressions. This lesson wires those gates into two of the most important circuits in all of computing. The first family — adder circuits — performs binary arithmetic, turning the abstract idea of binary addition into real hardware: a half adder, then a full adder built from it, then a ripple-carry adder for multi-bit numbers. The second family — flip-flops — does something gates alone cannot: it remembers. A flip-flop is a bistable circuit that stores one bit, and arrays of them form the registers and memory at the heart of the processor. We give special attention to the D-type flip-flop and edge-triggering, because that is the building block of the CPU registers you met in earlier lessons. Throughout, circuits are drawn as gate diagrams with their truth tables.
This lesson addresses the AQA A-Level Computer Science (7517) specification within §4.6 Fundamentals of computer systems, specifically §4.6.4 logic gates and Boolean circuits — constructing and analysing the half adder and full adder from logic gates with their truth tables and Boolean expressions, and understanding the D-type flip-flop as an edge-triggered store of a single bit and its role in registers and memory.
It builds directly on §4.6.5 Boolean algebra (the previous lesson supplies the gates and expressions) and on §4.5.4 binary addition, and links forwards to §4.7.3 the processor, since the registers it describes (PC, MAR, MDR, ACC) are arrays of D-type flip-flops.
A half adder adds two single-bit binary numbers, producing a sum (S) and a carry-out (C). The starting point is to think about adding two bits by hand: 0+0=0, 0+1=1, 1+0=1, and 1+1=102 — which is "0 carry 1". A single bit cannot represent the result of 1+1 on its own, so the adder needs two output bits: a sum and a carry into the next column.
| A | B | Sum (S) | Carry (C) |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 1 |
Read the columns against the gates from the previous lesson. The sum column is 1 exactly when the inputs differ (01 and 10) — that is precisely the XOR function. The carry column is 1 only when both inputs are 1 — that is precisely the AND function. Recognising the two output columns as two familiar gates is the whole trick to deriving an adder. Hence the Boolean expressions:
S=A⊕BC=A⋅B
flowchart LR
A((A)) --> XOR{{"XOR"}}
B((B)) --> XOR
XOR --> S["S (Sum)"]
A --> AND{{"AND"}}
B --> AND
AND --> C["C (Carry)"]
It is called a half adder because it has no carry input — it cannot accept a carry coming in from a lower-order column, so on its own it can only add the least-significant pair of bits. When you add multi-bit numbers by hand, every column except the rightmost may receive a carry from the column to its right; the half adder has nowhere to feed such a carry in, so it is incomplete for any column but the first. To chain additions across all the columns we need a circuit with a carry input as well as a carry output — the full adder.
A full adder adds two single-bit numbers plus a carry input (Cin) arriving from the previous (lower-order) stage, producing a sum (S) and a carry-out (Cout).
| A | B | Cin | Sum (S) | Cout |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 1 | 0 |
| 0 | 1 | 0 | 1 | 0 |
| 0 | 1 | 1 | 0 | 1 |
| 1 | 0 | 0 | 1 | 0 |
| 1 | 0 | 1 | 0 | 1 |
| 1 | 1 | 0 | 0 | 1 |
| 1 | 1 | 1 | 1 | 1 |
S=A⊕B⊕Cin
Cout=(A⋅B)+(Cin⋅(A⊕B))
The sum is 1 whenever an odd number of the three inputs are 1 (the XOR of all three). The carry-out is 1 when two or more inputs are 1 — exactly the majority function derived in the previous lesson, here expressed compactly using the A⊕B term.
It is worth verifying that the carry-out expression matches the truth table rather than taking it on trust. Test the row A=0,B=1,Cin=1, where the table says Cout=1:
Cout=(0⋅1)+(1⋅(0⊕1))=0+(1⋅1)=1.✓
Now test A=1,B=0,Cin=0, where the table says Cout=0:
Cout=(1⋅0)+(0⋅(1⊕0))=0+(0⋅1)=0.✓
The expression reads naturally: a carry is produced either when both A and B are 1 (the A⋅B term), or when exactly one of A and B is 1 and a carry came in (the Cin⋅(A⊕B) term). Checking an expression against two or three rows of its truth table like this is a habit that catches algebra slips in the exam.
A full adder can be assembled from two half adders and an OR gate:
flowchart LR
A((A)) --> HA1[["Half Adder 1"]]
B((B)) --> HA1
HA1 -- "partial sum" --> HA2[["Half Adder 2"]]
Cin((C_in)) --> HA2
HA2 --> S["S (Sum)"]
HA1 -- "Carry 1" --> OR{{"OR"}}
HA2 -- "Carry 2" --> OR
OR --> Cout["C_out"]
This is a perfect illustration of the previous lesson's theme: a more complex function (the full adder) is composed from simpler reusable blocks (half adders) wired with one extra gate.
To add multi-bit numbers, full adders are chained, the carry-out of each stage feeding the carry-in of the next. This is a ripple-carry adder. For a 4-bit addition of A3A2A1A0+B3B2B1B0:
flowchart TB
C0(("C_in = 0")) --> FA0[["FA0 (A0,B0)"]]
FA0 --> S0["S0"]
FA0 -- "carry" --> FA1[["FA1 (A1,B1)"]]
FA1 --> S1["S1"]
FA1 -- "carry" --> FA2[["FA2 (A2,B2)"]]
FA2 --> S2["S2"]
FA2 -- "carry" --> FA3[["FA3 (A3,B3)"]]
FA3 --> S3["S3"]
FA3 -- "carry" --> OV["C_out (overflow)"]
The lowest stage's carry-in is fixed at 0. The carry then ripples upward from stage to stage, exactly mirroring how you carry by hand when adding binary columns left along the place values.
Let us add A=10112 (decimal 11) and B=01102 (decimal 6); the correct answer is 100012 (decimal 17, needing a fifth bit). Trace the carry stage by stage from the least-significant bit (A0,B0) upward, with the bottom carry-in fixed at 0:
| Stage | Ai | Bi | Carry in | Sum bit Si | Carry out |
|---|---|---|---|---|---|
| FA0 | 1 | 0 | 0 | 1 | 0 |
| FA1 | 1 | 1 | 0 | 0 | 1 |
| FA2 | 0 | 1 | 1 | 0 | 1 |
| FA3 | 1 | 0 | 1 | 0 | 1 |
Reading the sum bits from FA3 down to FA0 gives 0001, and the final carry-out is 1, so the full result is 100012=17 — correct. Notice how the carry generated at FA1 had to ripple into FA2, whose carry rippled into FA3: FA3 could not produce its final answer until that chain had settled. This is the carry propagation delay in action, and the worked example shows concretely why a wide adder is only as fast as its slowest carry chain.
The final carry-out doubles as an overflow indicator: here it signals that the true 5-bit answer does not fit in 4 bits. In a fixed-width processor this is exactly how unsigned overflow is detected, linking the adder directly to the arithmetic limits of a given word size.
Limitation. Because each stage cannot compute its true output until the carry from the stage below has settled, the carry must propagate through all n stages sequentially. For an n-bit adder this carry propagation delay grows with n, limiting how fast a wide adder can run.
Solution. A carry look-ahead adder computes all the carry bits in parallel using extra logic derived from "generate" and "propagate" signals, slashing the delay at the cost of more gates — the classic speed-versus-hardware trade-off.
Gates alone are combinational — their output depends only on the current inputs and they store nothing the instant the inputs change. Yet a computer plainly remembers things: the value in a register persists from one instruction to the next. That memory comes from a different kind of circuit. A flip-flop is sequential: it is a bistable circuit (two stable states) that holds one bit even after the inputs change, thanks to feedback — outputs wired back to inputs so the circuit reinforces its own state. Flip-flops are the building blocks of registers, counters and static memory. We meet three: the SR, the D-type (the most important for registers) and the JK.
The SR flip-flop has inputs S (Set) and R (Reset) and outputs Q and Q (always the complement of Q).
| S | R | Q (next) | Q (next) | Action |
|---|---|---|---|---|
| 0 | 0 | Q | Q | No change (hold the stored bit) |
| 0 | 1 | 0 | 1 | Reset (Q ← 0) |
| 1 | 0 | 1 | 0 | Set (Q ← 1) |
| 1 | 1 | ? | ? | Invalid — forces both outputs to 0, contradicting Q=Q |
It is built from two cross-coupled NOR gates (or NAND gates), where each gate's output feeds back into the other:
flowchart LR
S((S)) --> NOR1{{"NOR"}}
R((R)) --> NOR2{{"NOR"}}
NOR1 --> Q["Q"]
NOR2 --> Qb["Q-bar"]
Qb -- "feedback" --> NOR1
Q -- "feedback" --> NOR2
It is the feedback that gives the circuit memory. Trace it through: if the flip-flop is currently storing Q=1 (so Q=0) and both inputs are 0, then the bottom NOR sees inputs R=0 and Q=1, outputting 0 (which is Q, correct); the top NOR sees S=0 and Q=0, outputting 1 (which is Q, correct). The two gates confirm each other's outputs, so the state is stable and holds indefinitely — that is how a handful of gates remembers a bit with no clock at all. Pulsing S to 1 forces Q to 1 (Set); pulsing R to 1 forces Q to 0 (Reset); and in each case the feedback then locks the new value in place when the input returns to 0. This self-reinforcing feedback loop is the essence of every storage element in a computer.
Exam Tip: For the invalid state (S = 1, R = 1), explain that both Q and Q are driven to 0, which violates the rule that they must be complementary. Worse, if both inputs then return to 0 simultaneously, the final state is unpredictable — a race condition. State both the contradiction and the race condition for full marks.
The D-type flip-flop removes the invalid state entirely and is the workhorse of CPU registers. It has a single data input D and a clock input, and it copies D to Q only at the edge of the clock.
| Clock | D | Q (next) | Action |
|---|---|---|---|
| Rising edge | 0 | 0 | Q captures D (= 0) |
| Rising edge | 1 | 1 | Q captures D (= 1) |
| No edge (steady clock) | X | Q | No change — holds the stored bit |
The phrase edge-triggered is crucial: the flip-flop samples D only at the instant the clock changes (here, the rising edge from 0 to 1), then holds that value steady until the next edge regardless of how D wobbles in between. This is what makes synchronous digital systems reliable — every flip-flop in the machine updates in lockstep on the same clock edge, so values are captured at well-defined moments rather than racing around uncontrolled.
flowchart LR
D((D)) --> DFF[["D-type<br/>flip-flop"]]
CLK((Clock)) --> DFF
DFF --> Q["Q (stored bit)"]
DFF --> Qb["Q-bar"]
To see edge-triggering concretely, trace a D flip-flop where the input D changes between clock edges. Only the value present at each rising edge is captured; changes in between are ignored:
| Time | Rising edge? | D at that moment | Q after |
|---|---|---|---|
| t1 | Yes | 1 | 1 |
| (between) | No | D wobbles to 0 then 1 | 1 (held — edges ignored) |
| t2 | Yes | 0 | 0 |
| (between) | No | D wobbles to 1 | 0 (held) |
| t3 | Yes | 1 | 1 |
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.