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

Code Breakdown

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).

	.include "m328pdef.inc"

The next few lines introduce a new assembler directive .def. The .def 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

Using .def also 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 the .def directive 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 .def 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

Conclusion

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.

rjhcoding.com 2018