Explore AVR assembly language

Let’s explore the AVR microcontroller along with its assembly language. We’ll develop an assembly program designed to make the LED on an Arduino Uno board flash on and off.

Story

Assembler programming operates at the hardware level, employing a concise set of instructions to execute mathematical operations and manipulate binary data. At this stratum, our commands directly control the state of circuits.

Avrasm2, developed by Atmel (now MicroChip), is the designated program for their AVR product line. Installation takes place on Windows systems during the setup of MicroChip Studio IDE or the older Atmel Studio Suite. Comprehensive documentation for AVRASM2 is available online.

For this particular project, I will utilize the Avra assembler on a Linux environment. Our approach involves a text editor for composing the source code and the command-line interface (CLI) within the terminal window. It’s important to avoid using applications like MSWord, WordPad, or OpenOffice, as they can introduce hidden formatting characters to the text.

Let’s Code

We will employ the following program for our task. Roughly every second, the sixth pin on the microcontroller chip’s PORTB will alternate the state of the LED on the Arduino Uno board, causing it to blink on and off.

Arduino Output Circuit

At the outset of this project, you’ll notice an image of the Arduino Uno placed at the top. Direct your attention to PB5, which corresponds to pin 13 situated at the right edge of the board. Notably, the Uno features a built-in LED circuit.

The instructions within our program will be translated into sequences of 0’s and 1’s that actively regulate voltages. This control extends to determining the pins designated for output. Positioned at memory address 0x25, PORTB comprises 8 distinct memory circuits, functioning as a conduit for generating the binary representation of the PORTB value. On the other hand, PINB functions as an input, perceiving the voltage states existing at PORTB. Additionally, DDRB assumes the role of the Data Direction Register associated with PORTB, residing at memory address 0x24.

Presented here is a schematic outlining the internal circuitry of the microcontroller chip. Positioned as the initial pin of PORTB, PB0 corresponds to pin D8 on the Uno Board. Notably, PB5 is interlinked with the pre-existing LED circuit. In the context of the Arduino IDE, we can activate PB5 as an output using the command pinMode(LED_BUILTIN, OUTPUT).

In scenarios where output pin PB5 on the Arduino Uno is governed by DDRB = 1, the output circuitry pertinent to that specific pin becomes operational. Conversely, when DDRB = 0, the circuitry permits the pin to establish a connection with the PINB register, thus configuring it for voltage input.

Preparation

Retrieve the attached blink.asm and m328pdef.inc files from this project. Establish a fresh directory and relocate the two aforementioned files into this newly created folder.

 

Launch the blink.asm file using a simple text editor, such as Notepad.

The AVR assembly language comprises a concise set of instructions. Terms like “inc,” “dec,” “clr,” or “ldi” hold specific meanings within the assembler program and can only be used for their designated purposes, such as increment, decrement, load immediate, and so on.

In our program, we make use of DDRB and PORTB. But where do these terms originate, and what do they signify? Surprisingly, they aren’t inherent to the Assembly language itself.

Assembly language empowers us to substitute these terms with corresponding values. To facilitate this, a definition file exists for the Atmega328p, the microcontroller utilized in the Arduino Uno. A slightly modified definition file has been created for the Atmega8 chip.

You’ll find the definition file pertinent to our exercise attached. Open this file using a text editor and search for DDRB and PORTB. Upon doing so, you’ll encounter directives that establish equivalencies between different elements. For instance, if you look up PB5, you’ll discover:

.equ    PB5    = 5    ; For compatibility

The significance of this line to the assembler is that when it encounters PB5 within the program, it should interpret it as the value 5. We will revisit this aspect later in our project.

Registers

The registers r0 to r31 are situated close to the core of the processor. Each of these registers functions as an 8-bit storage unit, enabling the controller to execute mathematical operations. You can envision them akin to 8-bit abacuses, with individual bits replacing the traditional beads.

Following this, we proceed to define certain terms—labels—that assist us in recollecting and comprehending the code. This enhances the logical structure of our program. For instance, r16 is assigned the label “mask,” holding a value that we will employ to activate the thirteenth pin on the Atmel MCU device.

As for r17, it governs whether the LED is in an active or inactive state, hence we name it “ledR.” Meanwhile, r18, r24, and r25 store numerical values that will decrement to introduce a delay. By assigning these registers names within our program, we’re making an effort to render our code more accessible and understandable for us humans.

Loop Values

The values designated for the delay are denoted as “oVal” and “iVal.” When the assembler comes across these labels in our assembly code, it will refer to the memory locations where these assigned numbers reside.

But wait a moment! Keep in mind that 8-bit numbers can only reach up to 255. So, how do we manage calculations involving larger values?

The AVR microcontroller is equipped to handle numbers with 16 bits. This means we can perform operations like counting, multiplication, and division with numbers that go as high as 65025. Later in the program, we will encounter special reserved keywords like “HIGH” and “LOW” that play a role in implementing 16-bit mathematics.

Specifically, “oVal” corresponds to the decimal value 71, while “iVal” represents decimal 28168. As a result, the delay values translate to:

.equ    oVal    = 71        ; outer loop value
.equ    iVal    = 28168     ; inner loop value

Our aim is to have the Arduino perform a countdown to zero over the course of approximately one second. To achieve this, you can conduct a Google search for AVR Assembly and these specific numbers. Through this search, you’ll discover comprehensive calculations detailing the time it takes for the commands to execute.

 

 


        clr    ledR
        ldi    mask, 0x20
        out    DDRB, mask

Initially, we perform a clear operation on ledR. By establishing ledR as an alias for r17, we effectively reset r17 to contain all zeros. Transmitting this value to the device register PORTB results in the LED on the Arduino board being switched off.

As for “mask,” which we’ve designated as an alternative name for r16, we utilize its value to influence DDRB. This action is instrumental in configuring the Data Direction Register for PORTB, dictating whether the pins are set as inputs or outputs. The command “out DDRB, 0xFF” configures all 8 pins as outputs. However, altering this line to “ldi mask, 0x20” or “0b00100000” restricts the action to activating solely the sixth pin on PORTB.

Take a moment to ponder the Uno’s digital Pin 13 within the Arduino IDE. It indeed corresponds to the sixth pin of PORTB. Owing to our numbering convention that commences with 0, the sequence progresses as 0, 1, 2… Consequently, the number 5 effectively signifies the sixth position. How about engaging in some Arduino math? Consider performing a left shift operation on the binary value 1, moving it 5 positions to the left.

 

 

; shift a 1 five positions to the left
    clr    ledR
    ldi    mask, (1<<PB5)
    out    DDRB, mask

The functionality is achieved thanks to the definition in the m328pdef.inc file, which establishes that PB5 corresponds to the value 5. With “1 << 5,” the binary digit representing one is shifted 5 positions to the left, transforming 0b00000001 into 0b00100000, and this binary value is expressed as 0x20 in hexadecimal notation.

Start

The term “start” serves as a label, and we could assign any name to it. However, opting for a meaningful label aids in comprehending the context, and it allows us to create a loop by concluding the program with the instruction “rjmp start.”

 

start:
    eor    ledR, mask
    out    PORTB, ledR

Within the count.asm file, we send the value assigned to the label “ledR” (which resides in register r17) to PORTB. Following this, we increment the value of ledR and proceed with the program’s execution. Conversely, in blink.asm, we alternate the value contained in r17 (named ledR) from 0b00000000 to 0b11111111.

The loop

This process involves taking numbers and decrementing them until they reach a value of zero. Once the value reaches zero, the processor exits the loop and proceeds to the subsequent command.

Now, let’s delve into the mechanics of how these loops operate.


        ldi	oLoopR,oVal	    ; initialize outer loop count 
oLoop: 
        ldi	iLoopRl,LOW(iVal)  ; intialize inner loop LOW
        ldi	iLoopRh,HIGH(iVal) ; intialize inner loop HIGH
iLoop:  
        sbiw	iLoopRl,1   ; decrement inner loop registers 
        brne	iLoop	    ; branch to iLoop if iLoop not 0 
        dec	oLoopR	    ; decrement outer loop register 
        brne	oLoop	    ; branch to oLoop if outer loop not 0 
        rjmp	start	    ; jump back to start

We initiate these loops by setting the initial values of oVal and iVal in registers r18 (for oVal), and r24 and r25 (for iVal). Within each iteration of the oVal loop, the sixteen-bit value in iVal is progressively decremented down to zero. Once both loops are completed, the program returns to the location marked as “start.”

Assembly

 

To create an output hex file, there are numerous options available. If you’re using a Windows PC, you can edit the assembly code using tools like MicroChip Studio, MPLabX, or CodeBlocks, and then assemble it using the Avrasm2 compiler included with MicroChip Studio.

However, for this project, let’s use the Avra assembler on a Linux system. Here’s how you can proceed:

  1. Open a terminal in the directory containing your .asm file and the m328pdef.inc definitions file.
  2. Enter the following command:
$ avra blink.asm

If everything proceeds smoothly, you should observe the following output:

Upon inspecting the directory housing the assembly source program, you should notice several new files that were generated during the assembly process. Specifically, keep an eye out for the file named “blink.hex.” It’s worth noting that any additional files that appear won’t have any impact on our project.

 

Connect an Arduino Uno to your computer and determine the assigned port. Then, in the directory, open a terminal and input:

 

$ avrdude -p atmega328p -c arduino -P /dev/ttyACM0 -U flash:w:count.hex:i

Employ Avrdude to seize the hex file and upload it onto the Arduino, which is connected to port ttyACM0. As this process takes place, you’ll observe the transmit and receive indicators on the board flickering, while the screen will display:

 

At this point, you should notice that the LED integrated into the Uno board is blinking at approximately a one-second interval.

 

.include "m328pdef.inc"
    .def	mask 	= r16	; mask register
    .def	ledR 	= r17	; led register
    .def	oLoopR 	= r18	; outer loop register
    .def	iLoopRl = r24	; inner loop register low
    .def	iLoopRh = r25	; inner loop register high
    .equ	oVal 	= 71	; outer loop value
    .equ	iVal 	= 28168	; inner loop value
    .cseg
    .org	0x00
    clr	    ledR		; clear led register to zero
    ldi	    mask,(1<<PB5)	; load the mask register 00100000
    out	    DDRB,mask		; turn on PB5 on PORTB as output
start:
    eor	    ledR, mask		; toggle value in led register 
    out     PORTB, ledR		; output ledR value to PORTB
    ldi     oLoopR,oVal		; initialize outer loop count
oLoop:
     ldi    iLoopRl,LOW(iVal)	; intialize inner loop count LOW
     ldi    iLoopRh,HIGH(iVal)	; intialize inner loop count HIGH
iLoop:
    sbiw    iLoopRl,1	        ; decrement inner loop register
    brne    iLoop		; branch to iLoop if iLoop register != 0
    dec	    oLoopR		; decrement outer loop register
    brne    oLoop		; branch to oLoop if oLoop register != 0
    rjmp    start		; jump back to start

 


About The Author

Muhammad Bilal

I am a highly skilled and motivated individual with a Master's degree in Computer Science. I have extensive experience in technical writing and a deep understanding of SEO practices.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top