Exercise 1 — 7-Segment Display Counter
This is your first real interfacing exercise. You will hold a physical component in your hand, understand why every wire goes where it goes, and write a program that drives it. By the end your display will count 0–9 in a loop.
Prerequisites — What to Buy
Before you sit down to build this circuit, collect every item in the table below. If even one piece is missing the circuit will not work. Read the "how to identify / what to ask for" column — this will help you at the local electronics shop where items may not be labelled clearly.
Components list
| # | Component | Qty | Approx. cost | How to identify / what to ask for |
|---|---|---|---|---|
| 1 | Arduino Uno R3 | 1 | ₹350–₹500 | Blue or black PCB, says "Arduino Uno" or "UNO R3". Has USB-B socket (square shape). Clones from China work fine. |
| 2 | 7-segment LED display — common cathode, single digit | 1 | ₹5–₹15 | 10-pin component, roughly 1.5 cm wide. Ask specifically for "common cathode" (not common anode). Part numbers: 5611AH, 3611BH, LTS-547AHR, or any "0.56 inch single digit common cathode". |
| 3 | 220 Ω resistors | 10 | ₹1–₹2 each | Colour bands: red – red – brown – gold. Sold in strips of 10 or 25 at any electronics shop. Buy a few extra — they are cheap and easy to lose. |
| 4 | Breadboard (full-size or half-size) | 1 | ₹60–₹120 | White plastic board with rows of holes. Full size (830 holes) is easier to work on. Look for firm, not wobbly, hole sockets. |
| 5 | Jumper wires (male-to-male) | 1 pack (≥20 wires) | ₹40–₹80 | Coloured short wires with metal pins on both ends. Sometimes called "dupont wires". A 40-piece pack is ideal. |
| 6 | USB cable (Type A to Type B) | 1 | ₹50–₹100 | The "printer cable" — rectangular connector on one end, square connector on the other. Required to program the Arduino from your PC. You may already have one. |
Total approximate cost: ₹550–₹800 (most of this is the Arduino Uno, which you will reuse for every exercise)
Tools you need
| Tool | What it is | Where to find |
|---|---|---|
| PC / laptop | Any Windows / Linux / Mac machine | You already have this |
| AVR-GCC or Arduino IDE | Free software to compile and upload code | arduino.cc/en/software — free download |
| Multimeter (optional but very helpful) | Measures voltage and continuity | ₹200–₹600 at any electronics shop. Ask for "digital multimeter DT830" |
Where to buy
- Local electronics markets: In most Indian cities these parts are available at your local electronics component shop (Lamington Road Mumbai, SP Road Bangalore, Chandni Chowk Delhi, Richie Street Chennai, Paltan Bazaar Pune). Walk in and ask by name. The 7-segment display is the only part where you must say "common cathode" explicitly.
- Online: Robu.in, Evelta.com, Probots.co.in ship within 3–5 days and carry everything in this list.
How to verify your display is common cathode before wiring
Do this quick test using one AA battery (1.5 V) and a single 220 Ω resistor:
- Connect the resistor from the + terminal of the battery to any of pins 1–5 or 6–9 on the display.
- Touch the – terminal to pin 3 or pin 8 (the middle bottom and middle top pins).
- If a segment lights up — you have a common cathode display. ✓
- If nothing lights up, try the other middle pin.
- If segments only light up when battery polarity is reversed — you have a common anode display. The wiring is different; do not use it for this exercise.
Never connect any segment pin directly to 5 V without a 220 Ω resistor in series. An LED has no internal resistance — without the resistor it will draw too much current and burn out within seconds.
What Is a 7-Segment Display?
Before touching any wire, understand what this component actually is.
A 7-segment display is not magic. It is simply seven small LEDs arranged in the shape of a figure-8. Each LED has a letter name: a, b, c, d, e, f, g. There is also a small LED for the decimal point, named dp.
By turning on different combinations of these LEDs, you can form digits 0–9 and some letters.
───── ← this is segment a (top horizontal bar)
| |
f → | | ← b (f = top-left vertical, b = top-right vertical)
| |
───── ← this is segment g (middle horizontal bar)
| |
e → | | ← c (e = bottom-left vertical, c = bottom-right vertical)
| |
───── • ← d = bottom bar, • = dp (decimal point)
That is the whole component. Seven LEDs, each with a name, each independently controllable.
What Each Digit Looks Like
Here is every digit 0–9 drawn using only those seven segments:
_ _ _ _ _ _ _ _
| | | _| _| |_| |_ |_ | _| |_|
|_| | |_ _| | _| |_| | |_| _|
0 1 2 3 4 5 6 7 8 9
Take digit 0 for example:
- The top bar
─is lit → segment a ON - Both verticals on the left
|are lit → segment f and segment e ON - Both verticals on the right
|are lit → segment b and segment c ON - The bottom bar
─is lit → segment d ON - The middle bar is not lit → segment g OFF
Take digit 1:
- Only the right two verticals are lit → segments b and c ON
- Everything else OFF
Take digit 7:
- Top bar + right two verticals → a, b, c ON, everything else OFF
Once you see this pattern, the table in the "Encoding" section will make complete sense.
The Physical Component
The display is a small plastic package with 10 pins — 5 on each side. Each pin connects to one LED inside.
┌────────────────┐
pin 1 ─┤ e f ├─ pin 9
pin 2 ─┤ d g ├─ pin 10
pin 3 ─┤ COM (cathode) ├─ pin 8 ← COM (cathode)
pin 4 ─┤ c b ├─ pin 6
pin 5 ─┤ dp a ├─ pin 7
└────────────────┘
(top view)
Notice that there are two COM pins (pins 3 and 8). They are both the same node — both go to GND. This is just for mechanical stability so the chip does not wobble in your breadboard.
[!TIP] How to find pin 1 on a DIP package: Hold the component so the text on it is readable. Pin 1 is at the bottom-left corner. Then count anti-clockwise: 1,2,3,4,5 down the left side, then 6,7,8,9,10 up the right side.
Understanding LEDs — From Scratch
Before you wire anything, you must understand what an LED is. This is essential — not optional.
LED = Light Emitting Diode. A diode only allows current to flow in one direction: from the positive leg (Anode) to the negative leg (Cathode).
Anode (+) Cathode (−)
│ │
▼ │
─────┤►├───────────── │
long leg short leg
(positive) (negative)
To light up an LED you need:
- Voltage across it — enough to overcome its forward voltage (≈ 2.0 V for red/yellow, ≈ 3.2 V for blue/white)
- Current through it — typically 10–20 mA
- Current flowing the right direction — anode to cathode
Why You Must Always Use a Resistor
An LED has no built-in resistance. If you connect it directly from 5V to GND with no resistor, the current is theoretically unlimited — the LED will instantly burn out (current might spike to hundreds of mA for a millisecond). The resistor limits current to a safe value.
5V ──── [Resistor 220Ω] ──── [LED Anode] ──►── [LED Cathode] ──── GND
↑
Current flows → LED glows
Ohm's Law:
Supply voltage = 5.0 V
LED forward voltage = 2.0 V (voltage the LED "uses up")
Voltage across resistor = 5.0 − 2.0 = 3.0 V
Current = V / R = 3.0 V / 220 Ω = 13.6 mA ← safe and bright
Common Cathode vs Common Anode
A 7-segment display has 7 LEDs sharing their negative legs together at a common connection point. How that common point is wired defines the type:
Common Cathode (most common, used in this exercise)
All LED cathodes (−) are tied together and connected to GND.
MCU Pin ──── [220Ω] ──── Anode (+) ──►── Cathode (−) ──┐
MCU Pin ──── [220Ω] ──── Anode (+) ──►── Cathode (−) ──┤
MCU Pin ──── [220Ω] ──── Anode (+) ──►── Cathode (−) ──┤ ← all cathodes
... │ tied together
GND
To turn a segment ON → write HIGH (1) to the MCU pin → current flows → LED glows.
To turn a segment OFF → write LOW (0) → no current → LED dark.
Logic: 1 = ON, 0 = OFF ← natural, easy to remember.
Common Anode (inverted logic)
All LED anodes (+) are tied together and connected to VCC (5V).
VCC ──────────────────── Anode (+) ──►── Cathode (−) ──── [220Ω] ──── MCU Pin
VCC ──────────────────── Anode (+) ──►── Cathode (−) ──── [220Ω] ──── MCU Pin
To turn a segment ON → write LOW (0) to the MCU pin → current flows from VCC through LED to pin → LED glows.
Logic: 0 = ON, 1 = OFF ← inverted! Easy to get wrong.
Always check your display datasheet or test with a battery. Both types look identical from the outside. If your digits appear inverted (segment ON when you expect OFF), you have a common anode display and need to invert your lookup table values.
Quick test to identify your display type
Take a 3V coin cell (CR2032) and a 220Ω resistor. Touch the resistor + positive terminal of coin cell to one pin, and the negative terminal to the COM pin. If a segment lights up, the COM is the cathode → common cathode. If nothing lights but a different configuration does, it is common anode.
The Circuit — Wiring It Up
Connect the display to the ATmega328P as shown. We use PORTD (PD0–PD6) for segments a through g.
ATmega328P 7-Segment Display
(Arduino Uno) (Common Cathode)
PD0 (pin 2) ─── [220Ω] ──────── pin 7 (segment a)
PD1 (pin 3) ─── [220Ω] ──────── pin 6 (segment b)
PD2 (pin 4) ─── [220Ω] ──────── pin 4 (segment c)
PD3 (pin 5) ─── [220Ω] ──────── pin 2 (segment d)
PD4 (pin 6) ─── [220Ω] ──────── pin 1 (segment e)
PD5 (pin 11) ─── [220Ω] ──────── pin 9 (segment f)
PD6 (pin 12) ─── [220Ω] ──────── pin 10 (segment g)
GND ─────────────────── pin 3 (COM)
GND ─────────────────── pin 8 (COM — same node, both to GND)
The 220Ω resistors are not optional. Without them, each LED tries to pull as much current as it wants — potentially 100+ mA — through a pin rated for 40 mA maximum. In practice the LED burns out in milliseconds or the MCU pin is permanently damaged. Always put a resistor on every segment, every time.
Breadboard Layout Tips
- Place the 7-segment display straddling the centre gap of the breadboard — pins on each side go into separate rows
- Place each 220Ω resistor in the same row as its segment pin, and connect the other end of the resistor to a wire going to the MCU
- Connect both COM pins (3 and 8) to the GND rail of the breadboard
- Connect the GND rail to the GND pin of your Arduino
Encoding Digits as Bit Patterns
Now we need to figure out what byte value to write to PORTD to display each digit.
Our wiring puts:
- bit 0 of PORTD = PD0 = segment a
- bit 1 of PORTD = PD1 = segment b
- bit 2 of PORTD = PD2 = segment c
- bit 3 of PORTD = PD3 = segment d
- bit 4 of PORTD = PD4 = segment e
- bit 5 of PORTD = PD5 = segment f
- bit 6 of PORTD = PD6 = segment g
So if we want to display digit 0 (segments a,b,c,d,e,f ON — g OFF):
Bit position: 7 6 5 4 3 2 1 0
Segment: — g f e d c b a
For digit 0: 0 0 1 1 1 1 1 1 = 0x3F
↑
g is OFF
Let's work through a few by hand to make sure you see the pattern:
Digit 1 — only b and c are on:
Bit: 7 6 5 4 3 2 1 0
Seg: — g f e d c b a
0 0 0 0 0 1 1 0 = 0x06
Digit 7 — only a, b, c are on:
Bit: 7 6 5 4 3 2 1 0
Seg: — g f e d c b a
0 0 0 0 0 1 1 1 = 0x07
Digit 4 — b, c, f, g are on:
Bit: 7 6 5 4 3 2 1 0
Seg: — g f e d c b a
0 1 1 0 0 1 1 0 = 0x66
Full table for all digits (common cathode, 1 = segment ON):
| Digit | Visual | g | f | e | d | c | b | a | Hex |
|---|---|---|---|---|---|---|---|---|---|
| 0 | |_| outer ring |
0 | 1 | 1 | 1 | 1 | 1 | 1 | 0x3F |
| 1 | right side only | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0x06 |
| 2 | top+mid+bot, right-top, left-bot | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 0x5B |
| 3 | top+mid+bot, right side | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 0x4F |
| 4 | mid, right side, top-left | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0x66 |
| 5 | top+mid+bot, left-top, right-bot | 1 | 1 | 0 | 1 | 1 | 0 | 1 | 0x6D |
| 6 | all except top-right | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0x7D |
| 7 | top + right side | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0x07 |
| 8 | all segments | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0x7F |
| 9 | all except bottom-left | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 0x6F |
Let's visually verify the table against the digit shapes:
_ _ _ _ _ _ _ _
| | | _| _| |_| |_ |_ | _| |_|
|_| | |_ _| | _| |_| | |_| _|
0x3F 0x06 0x5B 0x4F 0x66 0x6D 0x7D 0x07 0x7F 0x6F
0 1 2 3 4 5 6 7 8 9
[!TIP] For a common anode display, invert every byte: 0x3F becomes 0xC0, 0x06 becomes 0xF9, and so on. Or in code, write
~valuebefore outputting to the port.
What Happens Inside the MCU When You Write to PORTD
Before writing code, understand what OUT PORTD, R16 actually does:
Writing one byte to PORTD simultaneously controls all 8 pins in one instruction. That is why a lookup table works perfectly — one table read + one port write = one digit displayed.
DDRD — Telling the MCU Which Pins Are Outputs
Every pin on the AVR can be either an input or an output. At reset, all pins default to input (high-impedance). Before driving the display you must configure them as outputs by writing to the Data Direction Register D (DDRD):
DDRD = 0xFF means: all 8 bits of PORTD are OUTPUTS
DDRD = 0x00 means: all 8 bits of PORTD are INPUTS
DDRD = 0x7F means: bits 0–6 outputs, bit 7 input
If you forget OUT DDRD, R16 and try to write to PORTD, the pins stay in input mode and nothing happens — a very common beginner bug.
The Program Flow
Why LPM? The Harvard Architecture in Practice
You might wonder: why not just store the table in a regular array? This is where the Harvard Architecture lesson becomes very real.
In C, this is handled with the PROGMEM keyword:
const uint8_t seg_table[] PROGMEM = {0x3F, 0x06, ...}; // stored in Flash
uint8_t val = pgm_read_byte(&seg_table[i]); // uses LPM internally
Without PROGMEM, the C startup code copies the table from Flash into SRAM at boot — wasting 10 bytes of your precious 2 KB SRAM for a table that never changes. On a small MCU, this matters.
Building the Program — Snippet by Snippet
We will write the program piece by piece. Each snippet introduces one new idea. Read the explanation before looking at the code.
Snippet 1 — The Segment Lookup Table
We need the 10 byte values (0x3F, 0x06 … 0x6F) stored somewhere the CPU can read at runtime. We put them directly in Flash memory using .db (define byte). This is the Harvard architecture in action: the table lives in code space, not SRAM.
; Wiring: PD0=seg a, PD1=b, PD2=c, PD3=d, PD4=e, PD5=f, PD6=g
; Common cathode — write 1 to a bit to turn that segment ON
seg_table:
.db 0x3F, 0x06 ; patterns for digit 0 and digit 1
.db 0x5B, 0x4F ; digit 2 and digit 3
.db 0x66, 0x6D ; digit 4 and digit 5
.db 0x7D, 0x07 ; digit 6 and digit 7
.db 0x7F, 0x6F ; digit 8 and digit 9
The .db directive tells the assembler: "place these raw bytes consecutively in Flash at the current address." Two bytes per line, 5 lines = 10 bytes total. The label seg_table remembers where in Flash the first byte lives — we will use it in the next snippet to load Z.
Snippet 2 — Stack Pointer Setup
Before any CALL instruction the Stack Pointer (SP) must point to the top of SRAM. At reset, SP = 0 — which is wrong. This is the very first thing main must do.
main:
LDI R16, HIGH(RAMEND) ; RAMEND = 0x08FF on ATmega328P
OUT SPH, R16 ; SP high byte
LDI R16, LOW(RAMEND)
OUT SPL, R16 ; SP low byte — now SP = 0x08FF ✓
RAMEND is defined in the device include file. HIGH() and LOW() extract the upper and lower byte of the 16-bit value. If you skip this, the first CALL will push the return address onto address 0x0000 — overwriting your reset vector — and the program will crash.
Snippet 3 — Configure PORTD as Output
After reset every pin defaults to input (high-impedance). Writing to PORTD while pins are in input mode does nothing — a very common beginner bug. Set DDRD = 0xFF to make all 8 PORTD pins outputs.
LDI R16, 0xFF
OUT DDRD, R16 ; all PORTD pins → output
We use 0xFF (all bits 1) because we need PD0–PD6 for segments a–g. PD7 is unused but setting it as output is harmless.
Snippet 4 — Point Z at the Table (Word Address → Byte Address)
LPM (Load Program Memory) reads Flash. It uses the Z register (R31:R30) as a byte address into Flash. Flash labels are word addresses by default (because AVR instructions are 16-bit words). To get the byte address, shift left by 1 (<< 1).
LDI ZH, HIGH(seg_table << 1) ; upper byte of byte address
LDI ZL, LOW(seg_table << 1) ; lower byte of byte address
LDI R17, 0 ; digit counter, starts at 0
Think of it this way: if seg_table is at Flash word 50 (0x0032), the first byte is at byte address 100 (0x0064). The << 1 does that multiplication. Without it, LPM would read from the wrong location and display garbage.
Snippet 5 — The Main Display Loop
Now we loop forever: read one pattern byte from Flash, send it to PORTD, wait half a second, advance to the next digit.
loop:
LPM R16, Z+ ; read byte at Z into R16, then Z = Z+1
OUT PORTD, R16 ; all 7 segment pins update at once
CALL delay_500ms ; burn time — human eye needs ≥100ms per digit
INC R17 ; next digit (0→1→2→...→9)
CPI R17, 10 ; have we just finished digit 9?
BRNE loop ; no — go back for the next digit
; Digit 9 done — reset back to digit 0
LDI ZH, HIGH(seg_table << 1)
LDI ZL, LOW(seg_table << 1)
LDI R17, 0
RJMP loop
LPM R16, Z+ is the post-increment form: read the byte Z points to into R16, then add 1 to Z. So each iteration Z automatically advances to the next byte in the table — no extra ADIW Z, 1 needed.
When R17 reaches 10 (meaning we just displayed digit 9), we reset Z to the table start and R17 to 0, then jump back to loop.
Snippet 6 — The 500ms Delay Routine
There is no sleep(ms) in assembly. We burn CPU cycles in a counting loop. At 16 MHz, 500 ms = 8,000,000 cycles. We use a three-level nested loop so the inner counter fits in 8-bit registers.
delay_500ms:
LDI R20, 41 ; outer loop: 41 iterations at 16 MHz
outer:
LDI R19, 0 ; middle loop: 0 loads as 256 iterations
middle:
LDI R18, 0 ; inner loop: 256 iterations
inner:
DEC R18 ; 1 cycle
BRNE inner ; 2 cycles if taken, 1 if not
DEC R19
BRNE middle
DEC R20
BRNE outer
RET
Why does LDI R18, 0 give 256 iterations? DEC on 0x00 wraps to 0xFF (255), then counts down 255 → 1 → 0, then BRNE falls through. That is 256 decrements total.
Total inner-loop cycles ≈ 41 × 256 × 256 × (1+2) ≈ 8.06 million cycles. Divided by 16 MHz ≈ 504 ms, plus a small amount of loop setup overhead. This is good enough for a human-visible counter. For precision timing, use a hardware timer instead of a busy-wait loop.
Complete Program
; ═══════════════════════════════════════════════════════
; seven_seg_counter.S — 7-Segment Display Counter
; ATmega328P @ 16 MHz
; Wiring: PD0=a, PD1=b, PD2=c, PD3=d, PD4=e, PD5=f, PD6=g
; Common cathode — 1 = segment ON, 0 = segment OFF
; Counts 0–9 in a loop, 500 ms per digit
; ═══════════════════════════════════════════════════════
.include "m328Pdef.inc"
; ── Reset vector ────────────────────────────────────────
.cseg
.org 0x0000
RJMP main
; ── Segment lookup table in Flash ───────────────────────
seg_table:
.db 0x3F, 0x06 ; 0, 1
.db 0x5B, 0x4F ; 2, 3
.db 0x66, 0x6D ; 4, 5
.db 0x7D, 0x07 ; 6, 7
.db 0x7F, 0x6F ; 8, 9
; ── main ────────────────────────────────────────────────
main:
LDI R16, HIGH(RAMEND)
OUT SPH, R16
LDI R16, LOW(RAMEND)
OUT SPL, R16
LDI R16, 0xFF
OUT DDRD, R16 ; all PORTD pins = outputs
LDI ZH, HIGH(seg_table << 1)
LDI ZL, LOW(seg_table << 1)
LDI R17, 0 ; digit counter
; ── main loop ───────────────────────────────────────────
loop:
LPM R16, Z+
OUT PORTD, R16
CALL delay_500ms
INC R17
CPI R17, 10
BRNE loop
LDI ZH, HIGH(seg_table << 1)
LDI ZL, LOW(seg_table << 1)
LDI R17, 0
RJMP loop
; ── 500 ms busy-wait delay ──────────────────────────────
delay_500ms:
LDI R20, 41
outer:
LDI R19, 0
middle:
LDI R18, 0
inner:
DEC R18
BRNE inner
DEC R19
BRNE middle
DEC R20
BRNE outer
RET
Testing and Debugging
Before you write code, test the circuit manually:
Extension Challenges
Challenge A — Count Down with a Button
Add a push button on PD7 with a 10 kΩ pull-up resistor to 5V (or use the internal pull-up: PORTD |= (1<<7)). When the button is held, count down 9→0. When released, count up 0→9.
Challenge B — Variable Speed
Add a second button. Each press cycles through speeds: 1s → 500ms → 250ms → 100ms → 1s. Store a speed index (0–3) in R18 and use a jump table to call the matching delay routine.
Challenge C — 2-Digit Multiplexed Counter (00–99)
Add a second display. Both displays share the same 7 segment wires (PORTD), but each has its own transistor-controlled enable pin (PB0 and PB1). The MCU rapidly switches: enable display 1 → show tens digit → 5ms → enable display 2 → show units digit → 5ms → repeat at 100 Hz. The human eye sees both displays as steady because of persistence of vision — flicker above 50 Hz is invisible.
Verification Checklist
Before you call the exercise complete, verify each item:
- With power off, both COM pins are connected to GND and every segment pin has its own 220 ohm series resistor.
- With a simple
PORTD = 0x7Ftest, all seven segments light and the decimal point stays off. - With single-bit tests,
0x01lights segmenta,0x02lightsb, and so on through0x40forg. - The counter shows
0,1,2,3,4,5,6,7,8,9in order, with no extra or missing segments. - The digit period is about half a second at a 16 MHz clock.
Common failure symptoms:
- Nothing lights: COM pins are not grounded, DDRD was not set, or the display is common anode.
- One segment never lights: wrong row on the breadboard, open resistor, or one PORTD wire is swapped.
- Digits look wrong but segments work individually: the lookup table does not match the physical wiring order.
- Display is too bright or MCU gets hot: a segment is connected without a current-limiting resistor.
Explained Solution
The solution works because the display wiring deliberately maps segment a through g to PORTD bit 0 through bit 6. Each lookup-table byte is therefore a direct image of the display: a 1 bit sources current through one resistor and one LED segment into the common cathode node at GND. DDRD = 0xFF makes the port pins outputs, LPM R16, Z+ reads the next Flash-table byte, and OUT PORTD, R16 updates all segments in one instruction. The digit counter R17 limits the table walk to ten entries, then reloads Z so the display starts again at zero.
Summary
Further Reading
- Microchip ATmega328P Datasheet - I/O ports, electrical limits, and program-memory access.
- AVR Instruction Set Manual -
LPM,OUT,BRNE, and register operations. - Arduino Uno Rev3 Documentation - board pin mapping and hardware reference.
Next Exercise: Exercise 2 — PWM LED Dimmer →