Loading header...

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:

  1. Connect the resistor from the + terminal of the battery to any of pins 1–5 or 6–9 on the display.
  2. Touch the terminal to pin 3 or pin 8 (the middle bottom and middle top pins).
  3. If a segment lights up — you have a common cathode display. ✓
  4. If nothing lights up, try the other middle pin.
  5. 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.

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:

  1. Voltage across it — enough to overcome its forward voltage (≈ 2.0 V for red/yellow, ≈ 3.2 V for blue/white)
  2. Current through it — typically 10–20 mA
  3. 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.

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)
flowchart LR classDef mcu fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef res fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef seg fill:#fee2e2,stroke:#dc2626,color:#7f1d1d classDef gnd fill:#f1f5f9,stroke:#475569,color:#1e293b subgraph MCU ["ATmega328P PORTD"] PD0["PD0 → segment a"]:::mcu PD1["PD1 → segment b"]:::mcu PD2["PD2 → segment c"]:::mcu PD3["PD3 → segment d"]:::mcu PD4["PD4 → segment e"]:::mcu PD5["PD5 → segment f"]:::mcu PD6["PD6 → segment g"]:::mcu end subgraph RES ["220Ω resistors\n(one per segment)"] R0["R"]:::res R1["R"]:::res R2["R"]:::res R3["R"]:::res R4["R"]:::res R5["R"]:::res R6["R"]:::res end subgraph DISP ["7-Segment Display\n(Common Cathode)"] SA["a — top bar"]:::seg SB["b — top right"]:::seg SC["c — bottom right"]:::seg SD["d — bottom bar"]:::seg SE["e — bottom left"]:::seg SF["f — top left"]:::seg SG["g — middle bar"]:::seg COM["COM — both COM pins"]:::gnd end PD0 --> R0 --> SA PD1 --> R1 --> SB PD2 --> R2 --> SC PD3 --> R3 --> SD PD4 --> R4 --> SE PD5 --> R5 --> SF PD6 --> R6 --> SG SA & SB & SC & SD & SE & SF & SG --> COM --> GND["GND"]:::gnd

Breadboard Layout Tips

  1. Place the 7-segment display straddling the centre gap of the breadboard — pins on each side go into separate rows
  2. 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
  3. Connect both COM pins (3 and 8) to the GND rail of the breadboard
  4. 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 ~value before outputting to the port.


What Happens Inside the MCU When You Write to PORTD

Before writing code, understand what OUT PORTD, R16 actually does:

flowchart TD classDef reg fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef on fill:#dcfce7,stroke:#16a34a,color:#14532d classDef off fill:#f1f5f9,stroke:#94a3b8,color:#64748b classDef result fill:#fee2e2,stroke:#dc2626,color:#7f1d1d R16["R16 = 0x3F → binary: 0 0 1 1 1 1 1 1\nYou write this one byte to PORTD"]:::reg R16 --> B7 & B6 & B5 & B4 & B3 & B2 & B1 & B0 B7["bit 7 = 0\nPD7 → LOW\n(unused)"]:::off B6["bit 6 = 0\nPD6 → LOW\nsegment g → OFF"]:::off B5["bit 5 = 1\nPD5 → HIGH\nsegment f → ON"]:::on B4["bit 4 = 1\nPD4 → HIGH\nsegment e → ON"]:::on B3["bit 3 = 1\nPD3 → HIGH\nsegment d → ON"]:::on B2["bit 2 = 1\nPD2 → HIGH\nsegment c → ON"]:::on B1["bit 1 = 1\nPD1 → HIGH\nsegment b → ON"]:::on B0["bit 0 = 1\nPD0 → HIGH\nsegment a → ON"]:::on B5 & B4 & B3 & B2 & B1 & B0 --> RESULT RESULT["Display shows digit 0\na b c d e f all lit — g is dark\n ─\n│ │\n─\n│ │\n ─"]:::result

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

flowchart TD classDef init fill:#e0e7ff,stroke:#4338ca,color:#312e81 classDef loop fill:#dcfce7,stroke:#16a34a,color:#14532d classDef io fill:#ffedd5,stroke:#ea580c,color:#9a3412 classDef delay fill:#f3e8ff,stroke:#9333ea,color:#581c87 classDef check fill:#fee2e2,stroke:#dc2626,color:#7f1d1d RESET["Power on / Reset\nMCU starts executing from address 0x0000"]:::init INIT_SP["Set up Stack Pointer\n(needed before any CALL instruction)"]:::init INIT_PORT["DDRD = 0xFF\n→ All PORTD pins = outputs"]:::init INIT_Z["Point Z register at seg_table\nin Flash memory"]:::init INIT_COUNT["R17 = 0\n(current digit counter)"]:::init LOOP["── main loop ──"]:::loop LOAD["LPM R16, Z+\nRead pattern byte from Flash table\nZ automatically steps to next byte"]:::io OUTPUT["OUT PORTD, R16\nAll 7 segment pins update instantly"]:::io DELAY["CALL delay_500ms\nBurn 8,000,000 clock cycles\n= 500 ms at 16 MHz"]:::delay INC["INC R17\nMove to next digit"]:::check CMP["CPI R17, 10\nHave we shown digit 9 already?"]:::check RESET_Z["Yes → reload Z to start of table\nReset R17 = 0\nStart over from digit 0"]:::check RESET --> INIT_SP --> INIT_PORT --> INIT_Z --> INIT_COUNT INIT_COUNT --> LOOP --> LOAD --> OUTPUT --> DELAY --> INC --> CMP CMP -->|"R17 < 10\n(still digits to show)"| LOOP CMP -->|"R17 = 10\n(all digits shown)"| RESET_Z --> LOOP

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.

flowchart TD classDef flash fill:#fee2e2,stroke:#dc2626,color:#7f1d1d classDef sram fill:#dcfce7,stroke:#16a34a,color:#14532d classDef cpu fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 CPU["CPU"]:::cpu FLASH["Flash Memory (32 KB)\nStores: your program code\nand .db data tables\nRead with: LPM instruction\n(special Flash-read instruction)"]:::flash SRAM["SRAM (2 KB)\nStores: variables, stack\nRead with: LD instruction\n(normal memory read)"]:::sram PROBLEM["❗ Problem: The CPU's data bus\ndoes NOT physically connect to Flash.\nYou cannot use LD to read from Flash.\nYou MUST use LPM.\n\nThis is Harvard architecture:\ncode memory and data memory\nare separate address spaces."]:::warn CPU -->|"LPM — read from Flash"| FLASH CPU -->|"LD/ST — read/write SRAM"| SRAM FLASH -.->|"no direct data bus path"| SRAM PROBLEM -.-> CPU

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:

flowchart TD classDef test fill:#dcfce7,stroke:#16a34a,color:#14532d classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d classDef ok fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a T1["Step 1: Power only\nConnect GND + 5V\nNothing should light up yet\n(DDRD defaults to input)"]:::test T2["Step 2: Arduino Serial Monitor\nIn setup(): DDRD = 0xFF;\nIn loop(): PORTD = 0xFF; delay(1000);\nPORTD = 0x00; delay(1000);"]:::test T3["All 7 segments flash on/off\nevery second?"]:::ok T4["Nothing lights up"]:::fail T5["Only some segments flash"]:::fail T6["Step 3: Test individual segments\nPORTD = 0x01; → only segment a\nPORTD = 0x02; → only segment b\n...etc"]:::test T1 --> T2 --> T3 --> T6 T2 --> T4 T2 --> T5 T4 --> FIX1["Check: COM pins to GND?\nPORTD written before DDRD?\nWires in correct rows?"]:::fail T5 --> FIX2["Check: which segments are missing?\nCount backwards — which MCU pin is that?\nResistor properly inserted?"]:::fail T6 --> DONE["All individual segments work\n→ run the counter program"]:::ok

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.

flowchart TD classDef btn fill:#f3e8ff,stroke:#9333ea,color:#581c87 classDef up fill:#dcfce7,stroke:#16a34a,color:#14532d classDef dn fill:#fee2e2,stroke:#dc2626,color:#7f1d1d READ["IN R18, PIND\nread all PORTD pin states"]:::btn SBRC["SBRC R18, 7\n(Skip if Bit 7 is Clear\n= skip if button pressed)"]:::btn UP["Count UP\nINC R17\nif R17 == 10 → R17 = 0"]:::up DN["Count DOWN\nDEC R17\nif R17 == 0xFF (underflow) → R17 = 9"]:::dn READ --> SBRC SBRC -->|"bit 7 = 1 (not pressed)"| UP SBRC -->|"bit 7 = 0 (pressed)"| DN

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.

sequenceDiagram participant CPU participant D1 as Display 1 (Tens) participant D2 as Display 2 (Units) participant Eye as Human Eye loop Every 10 ms (100 Hz) CPU->>D1: Enable D1 transistor (PB0=HIGH) CPU->>D1: Output tens digit to PORTD CPU->>CPU: Wait 5ms CPU->>D1: Disable D1 (PB0=LOW) — blank briefly CPU->>D2: Enable D2 transistor (PB1=HIGH) CPU->>D2: Output units digit to PORTD CPU->>CPU: Wait 5ms CPU->>D2: Disable D2 (PB1=LOW) Note over Eye: Sees both digits lit — cannot detect the switching end

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 = 0x7F test, all seven segments light and the decimal point stays off.
  • With single-bit tests, 0x01 lights segment a, 0x02 lights b, and so on through 0x40 for g.
  • The counter shows 0,1,2,3,4,5,6,7,8,9 in 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

flowchart TD classDef hw fill:#ffedd5,stroke:#ea580c,color:#9a3412 classDef sw fill:#dcfce7,stroke:#16a34a,color:#14532d classDef theory fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a H1["LED needs: forward voltage\n+ current-limiting resistor\n+ correct polarity"]:::hw H2["Common cathode: all (-) to GND\n1=ON, 0=OFF"]:::hw H3["220Ω per segment protects\nboth LED and MCU pin"]:::hw S1["DDRD = 0xFF sets\nall PORTD pins as outputs"]:::sw S2["OUT PORTD, R16\nupdates all 8 pins at once"]:::sw S3["LPM reads lookup tables\nstored in Flash memory"]:::sw T1["Harvard arch: data bus\ndoes not reach Flash\n→ must use LPM not LD"]:::theory T2["Multiplexing: share pins\nbetween displays at >50Hz\nfor persistence of vision"]:::theory

Further Reading

Next Exercise: Exercise 2 — PWM LED Dimmer →

Mind Map

mindmap root((7 Segment Counter)) Core idea Seven LEDs form digits Common cathode to GND PORTD bits drive segments Wiring PD0 to a PD1 to b PD2 to c PD3 to d PD4 to e PD5 to f PD6 to g One 220 ohm resistor per segment Calculations I equals Vs minus Vf over R 5V minus 2V over 220 ohm Current about 13.6 mA Delay about cycles over 16 MHz Firmware DDRD sets outputs PORTD writes all bits LPM reads Flash table Z plus walks bytes Checks COM pins to GND Single bit segment test Digit table matches wiring Resistors not bypassed Mistakes Common anode table Missing DDR setup Swapped rows No current limit