Published on
Published on
(Last revised

Count 0-9 using Arduino Uno (ATMega328P) in Assembly


The lab task assigned to us was to count from 00-99 using 7 Segment Display, the program must be written in Assembly. Fortunately, the 7 segment decoder was not enough for everyone in the lab, so the tasks are simplified to count from 0-9 only, and repeats.

As someone who familiar with high-level programming language like Java, Python, etc, coding in assembly is infuriating 😂. The sweet side is about Assembly allows us to 'talk' directly to the bare metal, removing the unnecessary abstraction, solve performance issues, code like a chad and understand how the hardware works.

Arduino 7 Segment cover photo

In this article, we're going to dive into how the programs work on ATMega328P.

What you'll need


  • Arduino Uno (ATMega328P)
  • 7-Segment Display
  • BCD to 7-Segment Decoder. Eg: HD74LS47 (for common anode) or HD74LS48 (for common cathode)
  • Breadboard & some jumper wires


Manual (Must have for reference)

Understanding the ports

Arduino Uno pinout diagram (credit Arnab Kumar Das)

Arduino Uno pinout

The ATMega328P has several I/O pins, which can be used as input or output. The pins are grouped into ports, which are labeled as PORTB, PORTC and PORTD. As you might notice in the image above, the lower left side is PD0 to PD7, then continued with PB0 to PB5 and so on.

Binary Coded Decimal (BCD)

Binary-coded decimal is a system of writing numerals that assigns a four-digit binary code to each digit 0 through 9 in a decimal (base 10) number. Note that BCD is not the same as binary representation. (Read more here) Both ICs I've mentioned above accept 4-bit BCD input, and outputs to 7-segment display.

Below is the truth table for the decoder. Note that in this article, I'm using the DCBA convention instead of ABCD. (Image credit: Digital Electronics)

BCD to 7-Segment Decoder Truth Table

Now, for example, we want to display a digit 4 on the 7-Segment display, how do we do that?

That's right. We send the 0100 to the decoder. Let's construct our circuit first.

Fritzing BCD counter 7 segment Atmega

The assembly code would be like this:


	LDI R16, 0xFF
	OUT DDRD, R16 ; Set PORTD as output
	LDI R18, 0X20
	OUT PORTD, R18 ; Pin 5 HIGH

With all of that, it should be displaying digit 4 now:

Digit 4 on 7-Segment lab

Maybe you were wondering, why 0x20? 0x20 is equivalent to 0010 0000 in binary, representing which PD𝑥 in PORT𝑥 to be set as HIGH. In this case, it's pin 5 (counting from 0).

explaination digit 4 binary

Ok, enough introductions, now back to our objective, which counts from 0 to 9 and display on the 7-Segment display.

Back to business

I have already gone through the struggle to make this work, so I'm just going to share the code with you, and explain it along the way.

	; initial counter value
	ldi r18, 0;

	; set PORTD to OUTPUT
	ldi r16, 0xff
	out ddrd, r16

	rcall main

	; Output value to GPIO
	mov r20, r18 ; Copy content r18 to r20 (temporary)
	; Shift bits (according to location of the pins)
	lsl r20
	lsl r20
	lsl r20
	out portd, r20

	rcall delay

	ldi r17, 1 ; increment
	add r18, r17 ; increment current counter

	cpi r18, 10 ; compare current counter if match 10
	breq reset ; if true, go to reset
	rjmp main ; if false, continue looping the 'main' block

	ldi r18, 0 ; reset counter to 0
	rjmp main

	ldi r24, 100
	ldi r25, 63
	ldi r26, 10

L1: dec  r26
    brne L1
    dec  r25
    brne L1
    dec  r24
    brne L1


Let's begin with the start subroutine. It's the first subroutine that will be executed when the program starts. We set the initial value of the counter to 0, and set the PORTD as output.

	ldi r18, 0;

	ldi r16, 0xff
	out ddrd, r16

	rcall main

Then, we jump to the main subroutine by using the rcall instruction.

Next, we copy the current counter value to register r20. Same like the example with digit 4 above, we want to map the counter value bits one-to-one with the PORTs. Since our port doesn't start from 0, the bits need be shifted. This can be done through the lsl instruction.

AVR LSL instruction

In case it wasn't clear, take a look at the diagram below. So, three lsl instructions there is equivalent with the pins that are empty.

shift bits lsl
	mov r20, r18

	lsl r20
	lsl r20
	lsl r20
	out portd, r20
Atmel AVR debugging

On Atmel Studio, you can set breakpoints etc to easily debug your program. The status register on the left panel is very useful to see the current register value. It's like a Serial.println for us.

Delay is necessary, or otherwise the program ran too fast, and we could not see the clear output

  rcall delay

Then, we load the increment value to the register r17. In the next line, using the add instruction, we add the increment value to the current counter value. The addition result is stored in r18 register.

AVR ADD instruction
    ldi r17, 1
    add r18, r17

Next, we do not want our counter to count indefinitely, so we need to reset it to 0 when it reaches 9+1. We can do this by using the cpi instruction. The next line, we used the breq (Branch if Equal) instruction to jump to the reset subroutine, otherwise, just loop from the beginning of main.

AVR CPI Instruction
    cpi r18, 10
	breq reset
	rjmp main

In reset subroutine, we just reset the counter value to 0 and jump back to the main subroutine.

    ldi r18, 0
    rjmp main

That's all. I'm not going to explain about the delay and L1 subroutines. But just keep in mind there are meant to be together, the delay depends on L1 (or vise versa, I'm not sure). Let's see how the results.


Closing remarks

I enjoy making this to work using Assembly. I hope you've learnt something from this article. Kindly visit here for more Arduino - Assembly examples.

Believe it or not; this code was my first attempt to complete this task as I don't have any idea how to do counter. I know, it is an awful code to look at it, consisting of 100+ LOC, but technically works 😆

Thank you for reading, for any thoughts or suggestions, please leave a comment below. I'll leave you with this meme from r/ProgrammerHumor.

stop doing assembly