Example Program #2: Adding 16-bit Numbers
Here is another example program to give us some context before looking at the instructions that operate on general purpose working registers.
In this tutorial, we will look at the process of adding two 16-bit numbers. The code for this is shown below.
.include "m328pdef.inc" .def num1L = r16 ; define lower byte of number 1 as r16 .def num1H = r17 ; define upper byte of number 1 as r17 .def num2L = r18 ; define lower byte of number 2 as r18 .def num2H = r19 ; define upper byte of number 2 as r19 .cseg .org 0x00 ldi num1L,0x34 ; load 0x34 into r16 ldi num1H,0x12 ; load 0x12 into r17 ldi num2L,0xCD ; load 0xCD into r18 ldi num2H,0xAB ; load 0xAB into r19 add num1L,num2L ; add lower bytes of number adc num2H,num2H ; add upper bytes of number loop: rjmp loop ; infinite loop
As before, our program begins with an include file that contains definitions specific to our microcontroller (of course, make sure to include the file specific to the chip you are using).
The next few lines introduce a new assembler directive. The directive allows us to rename a register with a more descriptive mnemonic. This mnemonic can then be used later in the code and the assembler will automatically replace the mnemonic with the register.
.def num1L = r16 ; define lower byte of number 1 as r16 .def num1H = r17 ; define upper byte of number 1 as r17 .def num2L = r18 ; define lower byte of number 1 as r18 .def num2H = r19 ; define upper byte of number 1 as r19
Usingalso allows an easy way to change the register used for a particular function. By defining it in one place, the register can be swapped later if you change your mind or realize you need that register for something else.
Use thedirective to assign registers a meaningful name. This will make your program easier to read and understand.
Now that we are about to start writing actual instructions, we specify that we are in code segment starting at the beginning of flash.
.cseg .org 0x00
We need to get the numbers we want to add into our registers, so we will use the ldi instruction. In this case, we will add 0x1234 and 0xABCD
ldi num1L,0x34 ; load 0x34 into r16 ldi num1H,0x12 ; load 0x12 into r17 ldi num2L,0xCD ; load 0x78 into r18 ldi num2H,0xAB ; load 0x56 into r19
0x1234 and 0xABCD are 16-bit numbers, so they each need two registers to hold them. Since we defined our registers previously using the directive, it is more clear what the purpose of each register is - a high and a low byte for our first number and our second number.
The AVR Architecture cannot directly add two 16-bit numbers stored in registers with a single instruction, but it can do it in two. We start by adding the lower bytes of our 16-bit numbers using the add instruction.
The instruction add takes two registers as operands (they can be any of the 32 available), adds their contents, and stores the result in the first operand. In the following, num1L is added to num2L and num1L is overwritten with the result.
add num1L,num2L ; add lower bytes of number
Following this, we use the instruction adc - add with carry - to add the upper bytes of our 16-bit numbers. adc is used just like add, taking two registers as operands, computing their sum, and storing the result in the first register. However, adc has the special added feature that it knows whether the previous add instruction resulted in an overflow. If it did, adc will carry an extra bit into the sum to account for this.
adc num1H,num2H ; add upper bytes of number
When the addition operations are complete, num1H and num1L will contain the value 0xBE01, which is the sum of 0x1234 and 0xABCD.
Previous instructions will not affect the operation of add. However, if the carry flag is set by a previous instruction, adc will include it in its sum.
What would happen if we used add instead of adc on the upper bytes? If this were the case, num1H would contain the value 0xBD, one less than the actual sum of 0x1234 and 0xABCD. In effect, we would be just be adding four unrelated 8-bit numbers instead of two 16-bit ones.
add num1L,num1H ; add lower bytes of number add num1H,num2H ; carry is not brought into upper byte sum
In the final line, we create an infinite loop to hold our microcontroller when the program finishes.
loop: rjmp loop ; infinite loop
And there you have it - two 16-bit numbers added together in assembly. It's not the most exciting program, but we're getting there. Hopefully now you see the use of the general purpose working registers and how to use them with instructions. In addition, you've seen how to use assembler directives to make your programs more readable and easier to modify.
Now it's time to dive into the multitude of instructions supported by the general purpose working registers.