Path:Home =>
AVR-overview =>
assembler introduction => Calculations
Beginner's introduction to AVR assembler language
Calculations in assembler language
Here we discuss all necessary commands for calculating in AVR assembler language.
This includes number systems, setting and
clearing bits, shift and rotate, and
adding/subtracting/comparing and the
format conversion of numbers.
The following formats of numbers are common in assembler:
The smallest whole number to be handled in assembler is a byte with eight bits. This
codes numbers between 0 and 255. Such bytes fit exactly into one register of the MCU.
All bigger numbers must be based on this basic format, using more than one register.
Two bytes yield a word (range from 0 .. 65,535), three bytes form a longer word
(range from 0 .. 16,777,215) and four bytes form a double word (range from 0 ..
4,294,967,295).
The single bytes of a word or a double word can be stored in whatever register you
prefer. Operations with these single bytes are programmed byte by byte, so you don't
have to put them in a row. In order to form a row for a double word we could store
it like this:
.DEF r16 = dw0
.DEF r17 = dw1
.DEF r18 = dw2
.DEF r19 = dw3
dw0 to dw3 are in a row in the registers. If we need to initiate this double word
at the beginning of an application (e.g. to 4,000,000), this should look like this:
.EQU dwi = 4000000 ; define the constant
LDI dw0,LOW(dwi) ; The lowest 8 bits to R16
LDI dw1,BYTE2(dwi) ; bits 8 .. 15 to R17
LDI dw2,BYTE3(dwi) ; bits 16 .. 23 to R18
LDI dw3,BYTE4(dwi) ; bits 24 .. 31 to R19
So we have splitted this decimal number called dwi to its binary portions and packed
them into the four byte packages. Now you can calculate with this double word.
To the top of that page
Sometimes, but in rare cases, you need negative numbers to calculate with. A negative
number is defined by interpreting the most significant bit of a byte as sign bit. If
it is 0 the number is positive. If it is 1 the number is negative. If the number is
negative we usually do not store the rest of the number as is, but we use its inverted
value. Inverted means that -1 as an byte integer is not written as 1,0000001 but as
1,1111111 instead. That means: subtract 1 from 0 and forget the overflow. The first
bit is the sign bit, signalling that this is a negative number. Why this different
format (subtracting the negative number from 0) is used is easy to understand: adding
-1 (1,1111111) and +1 (0,0000001) yields exactly zero, if you forget the overflow
that occurs during that operation (the nineth bit).
In one byte the biggest number to be handled is +127 (binary 0,1111111), the smallest
one is -128 (binary 1,0000000). In other computer languages this number format is
called short integer. If you need a bigger range of values you can add another byte
to form a normal integer value, ranging from +32,767 .. -32,768), four bytes provide
a range from +2,147,483,647 .. -2,147,483,648, usually called a LongInt or DoubleInt.
To the top of that page
Positive or signed whole numbers in the formats discussed above use the available space
most effectively. Another, less dense number format, but easier to handle is to store
decimal numbers in a byte for one digit each. The decimal digit is stored in its
binary form in a byte. Each digit from 0 .. 9 needs four bits (0000 .. 1001), the
upper four bits of the byte are zeros, blowing a lot of air into the byte. For to
handle the value 250 we would need at least three bytes, e.g.:
| Bit value | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
| R16, Digit 1 = 2 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
| R17, Digit 2 = 5 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 |
| R18, Digit 3 = 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Instructions to use:
LDI R16,2
LDI R17,5
LDI R18,0
You can calculate with these numbers, but this is a bit more complicated in assember
than calculating with binary values. The advantage of this format is that you can handle
as long numbers as you like, as long as you have enough storage space. The calculations
are as precise as you like (if you program AVRs for banking applications), and you can
convert them very easily to character strings.
To the top of that page
If you pack two decimal digits into one byte you don't loose that much storage space.
This method is called packed binary coded digits. The two parts of a byte are called
upper and lower nibble. The upper nibble usually holds the more significant digit,
which has advantages in calculations (special instructions in AVR assembler language).
The decimal number 250 would look like this when formatted as a packed BCD:
| Byte | Digits | Value | 8 | 4 | 2 | 1 | 8 | 4 | 2 | 1 |
| 2 | 4,3 | 02 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
| 1 | 2,1 | 50 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 |
Instructions for setting:
LDI R17,0x02 ; Upper byte
LDI R16,0x50 ; Lower byte
To set this correct you can use the binary notation (0b...) or the hexadecimal notation
(0x...) to set the proper bits to their correct nibble position.
Calculating with packed BCDs is a little more complicated compared to the binary
form. Format changes to character strings are as easy as with BCDs. Length of numbers
and precision of calculations is only limited by the storage space.
To the top of that page
Very similiar to the unpacked BCD format is to store numbers in ASCII format. The
digits 0 to 9 are stored using their ASCII (ASCII = American Standard Code for
Information Interchange) representation. ASCII is a very old format, develloped and
optimized for teletype writers, unnecessarily very complicated for computer use
(do you know what a char named End Of Transmission EOT meant when it was invented?),
very limited in range for other than US languages (only 7 bits per character), still
used in communications today due to the limited efforts of some operating system
programmers to switch to more effective string systems. The ancient system is only
topped by the european 5-bit long teletype character set called Baudot set or the
still used Morse code.
Within the ASCII code system the decimal digit 0 is represented by the number 48
(hex 0x30, binary 0b0011.0000), digit 9 is 57 decimal (hex 0x39, binary 0b0011.1001).
ASCII wasn't designed to have these numbers on the beginning of the code set as there
are already command chars like the above mentioned EOT for the teletype. So we still
have to add 48 to a BCD (or set bit 4 and 5 to 1) to convert a BCD to ASCII. ASCII
formatted numbers need the same storage space like BCDs. Loading 250 to a register
set representing that number would look like this:
LDI R18,'2'
LDI R17,'5'
LDI R16,'0'
The ASCII representation of these characters are written to the registers.
To the top of that page
To convert a BCD coded digit to its ASCII representation we need to set
bit 4 and 5 to a one. In other words we need to OR the BCD with a constant value of
hex 0x30. In assembler this is done like this:
ORI R1,0x30
If we have a register that is already set to hex 0x30 we can use the
OR with this register to convert the BCD:
OR R1,R2
Back from an ASCII character to a BCD is a bit more complicated in AVR
assembler, because the instruction
ANDI R1,0x0F
that isolates the lower four bits (= the lower nibble) is only possible with registers
above R15. If you need to do this, use one of the registers R16 to R31!
If the hex value 0x0F is already in register R2, you can AND the ASCII
character with this register:
AND R1,R2
The other instructions for manipulating bits in a register are also
limited for registers above R15. They would be formulated like this:
SBR R16,0b00110000 ; Set bits 4 und 5 to one
CBR R16,0b00110000 ; Clear bits 4 and 5 to zero
If one or more bits of a byte have to be inverted you can use the
following instruction (which is not possible for use with a constant):
LDI R16,0b10101010 ; Invert all even bits
EOR R1,R16 ; in register R1 and store result in R1
To invert all bits of a byte is called the One's complement:
COM R1
inverts the content in register R1 and replaces zeros by one and vice versa. Different
from that is the Two's complement, which converts a positive signed number to its
negative complement (subracting from zero). This is done with the instruction
NEG R1
So +1 (decimal: 1) yields -1 (binary 1.1111111), +2 yields -2 (binary 1.1111110),
and so on.
Besides the manipulation of the bits in a register copying a single bit
is possible using the so-called T-bit of the status register. With
BLD R1,0
the T-bit is loaded to bit 0 of register R1.
The T-bit can be set or cleared and then copy its content to any bit
in any register:
CLT ; clear T-bit, or
SET ; set T-bit, or
BST R2,2 ; copy register R2, bit 2, to the T-bit
To the top of that page
Shifting and rotating of binary numbers means multiplicating and dividing them by 2.
Shifting has several sub-instructions.
Multiplication with 2 is easily done by shifting all bits of a byte one
binary digit left and writing a zero to the least significant bit. This is called
logical shift left. The former bit 7 of the byte will be shiftet to the carry bit in
the status register.
LSL R1
The inverse division by 2 is the instruction called logical shift right.
LSR R1
The former bit 7, now shifted to bit 6, is filled with a 0, while the former bit 0 is
shifted into the carry bit of the status register. This carry bit could be used to
round up and down (if set, add one to the result). Example, division by four with
rounding:
LSR R1 ; division by 2
BRCC Div2 ; Jump if no round up
INC R1 ; round up
Div2:
LSR R1 ; Once again division by 2
BRCC DivE ; Jump if no round up
INC R1 ; Round Up
DivE:
So, dividing is easy with binaries as long as you divide by multiples of 2.
If signed integers are used the logical shift right would overwrite
the sign-bit in bit 7. The instruction arithmetic shift right leaves bit 7 untouched
and shifts the 7 lower bits, inserting a zero in bit 6.
ASR R1
Like with logical shifting the former bit 0 goes to the carry bit in the status
register.
What about multiplying a 16-bit word by 2? The most significant bit
of the lower byte has to be shifted to yield the lowest bit of the upper byte. In
that step a shift would set the lowest bit to zero, but we need to shift the carry
bit from the previous shift of the lower byte into bit 0. This is called a rotate.
During rotation the carry bit in the status register is shifted to bit 0, the former
bit 7 is shifted to the carry during rotation.
LSL R1 ; Logical Shift Left of the lower byte
ROL R2 ; ROtate Left of the upper byte
The logical shift left in the first instruction shifts bit 7 to carry, the ROL
instruction rolls it to bit 0 of the upper byte. Following the second instruction
the carry bit has the former bit 7. The carry bit can be used to either indicate an
overflow (if 16-bit-calculation is performed) or to roll it into upper bytes (if more
than 16 bit calculation is done).
Rolling to the right is also possible, dividing by 2 and shifting carry to
bit 7 of the result:
LSR R2 ; Logical Shift Right, bit 0 to carry
ROR R1 ; ROtate Right and shift carry in bit 7
It's easy dividing with big numbers. You see that learning assembler is not THAT
complicated.
The last instruction that shifts four bits in one step is very often
used with packed BCDs. This instruction shifts a whole nibble from the upper to the
lower position and vice versa. In our example we need to shift the upper nibble to
the lower nibble position. Instead of using
ROR R1
ROR R1
ROR R1
ROR R1
we can perform that with a single
SWAP R1
This exchanges the upper and lower nibble. Note that the upper nibble's content will be
different after applying these two methods.
To the top of that page
The following calculation operations are too complicated for the beginners and
demonstrate that assembler is only for extreme experts, hi. Read on your own risk!
To start complicated we add two 16-bit-numbers in R1:R2 and R3:R4. In
this notation we mean that the first register is the most signifant byte, the second
the least significant.
ADD R2,R4 ; first add the two low-bytes
ADC R1,R3 ; then the two high-bytes
Instead of a second ADD we use ADC in the second instruction. That means
add with carry, which is set or cleared during the first instruction, depending from
the result. Already scared enough by that complicated math? If not: take this!
We subtract R3:R4 from R1:R2.
SUB R2,R4 ; first the low-byte
SBC R1,R3 ; then the high-byte
Again the same trick: during the second instruction we subract another 1 from the
result if the result of the first instruction had an overflow. Still breathing? If yes,
cope with the following!
Now we compare a 16-bit-word in R1:R2 with the one in R3:R4 to evaluate
whether it is bigger than the second one. Instead of SUB we use the compare
instruction CP, instead of SBC we use CPC:
CP R2,R4 ; compare lower bytes
CPC R1,R3 ; compare upper bytes
If the carry flag is set now, R1:R2 is smaller than R3:R4.
Now we add some more complicated stuff. We compare the content of R16
with a constant: 0b10101010.
CPI R16,0xAA
If the Zero-bit in the status register is set after that, we know that R16 is 0xAA.
If the carry-bit is set, we know, it is smaller. If it is not set and the Zero-bit is
not set either, we know it is bigger.
And now the most complicated test. We evaluate whether R1 is zero or
negative:
TST R1
If the Z-bit is set the register R1 is zero and we can follow with the instructions
BREQ, BRNE, BRMI, BRPL, BRLO, BRSH, BRGE, BRLT, BRVC or BRVS to branch around a bit.
Still with us? If yes, here is some packed BCD calculations. Adding two packed BCDs
can result in two different overflows. The usual carry shows an overflow, if the higher
of the two nibbles overflows to more than 15 decimal. Another overflow, from the lower
to the upper nibble occurs, if the two lower nibbles add to more than 15 decimal. To
take an example we add the packed BCDs 49 (=hex 49) and 99 (=hex 99) to yield 148 (=hex
0x148). Adding these in binary math, results in a byte holding hex 0xE2, no byte overflow
occurs. The lower of the two nibbles has had an overflow because 9+9=18 and the lower
nibble can only handle numbers up to 15. The overflow was added to bit 4, the lowest
significant bit of the upper nibble. Which is correct! But the lower nibble should be 8
and is only 2 (18 = 0b0001.0010). We should add 6 to that nibble to yield a correct
result. Which is quite logic, because whenever the lower nibble reaches more than 9 we
have to add 6 to correct that nibble.
The upper nibble is totally incorrect, because it is 0xE and should be 3 (with a 1
overflowing to the next upper digit of the packed BCD). If we add 6 to this 0xE we
get to 0x4 and the carry is set (=0x14). So the trick is to add these two numbers and
then add 0x66 to correct the 2 digits of the packed BCD. But halt: what if adding the
first and the second number would not result in an overflow to the upper nibble? And
not result in a digit above 9 in the lower nibble? Adding 0x66 would then result in a
totally incorrect result. The lower 6 should only be added if the lower nibble either
overflows to the upper nibble or results in a digit greater than 9. The same with the
upper nibble.
How do we know, if an overflow from the lower to the upper nibble has occurred? The
MCU sets a H-bit in the status register, the half-carry bit. The following table shows
the different cases that are possible after adding R1 and R2 and adding hex 0x66 after
that.
Add R1,R2 (Half)Carry-Bit | Add Nibble,6 (Half)Carry-Bit | Correction |
| 0 | 0 | subtract 6 |
| 1 | 0 | none |
| 0 | 1 | none |
| 1 | 1 | (not possible) |
To program an example we assume that the two packed BCDs are in R2 and R3, R1 will hold
the overflow, and R16 and R17 are available for calculations. R16 is the adding register
for adding 0x66 (the register R2 cannot add a constant value), R17 is used to correct
the result depending from the different flags. Adding R2 and R3 goes like that:
LDI R16,0x66 ; for adding 0x66 to the result
LDI R17,0x66 ; for later subtracting from the result
ADD R2,R3 ; add the two two-digit-BCDs
BRCC NoCy1 ; jump if no byte overflow occurs
INC R1 ; increment the next higher byte
ANDI R17,0x0F ; don't subtract 6 from the higher nibble
NoCy1:
BRHC NoHc1 ; jump if no half-carry occured
ANDI R17,0xF0 ; don't subtract 6 from lower nibble
NoHc1:
ADD R2,R16 ; add 0x66 to result
BRCC NoCy2 ; jump if no carry occured
INC R1 ; increment the next higher byte
ANDI R17,0x0F ; don't subtract 6 from higher nibble
NoCy2:
BRHC NoHc2 ; jump if no half-carry occured
ANDI R17,0xF0 ; don't subtract 6 from lower nibble
NoHc2:
SUB R2,R17 ; subtract correction
A little bit shorter than that:
LDI R16,0x66
ADD R2,R16
ADD R2,R3
BRCC NoCy
INC R1
ANDI R16,0x0F
NoCy:
BRHC NoHc
ANDI R16,0xF0
NoCy:
SUB R2,R16
Question to think about: Why is that equally correct and where is the trick?
To the top of that page
All number formats can be converted to any other format. The conversion from BCD to
ASCII and vice versa was already shown above (Bit manipulations).
Conversion of packed BCDs is not very complicated either. First we have to copy the
number to another register. With the copied value we change nibbles with SWAP to
exchange the upper and the lower one. The upper part is cleared, e.g. by ANDing
with 0x0F. Now we have the BCD of the upper nibble and we can either use as is or set
bit 4 and 5 to convert to an ASCII character. After that we copy the byte again and
treat the lower nibble without first SWAPping and get the lower BCD.
A little bit more complicated is the conversion of BCD digits to a binary. Depending
on the numbers to be handled we first clear the necessary bytes that will hold the
result of the conversion. We then start with the highest BCD digit. Before adding this
to the result we multiply the result with 10. In order to do this we copy the result
to somewhere else. Then we multiply the result by four (two left shifts resp. rolls).
Adding the previously copied result to this yields a multiplication with 5. Now a
mulitiplication with 2 (left shift/roll) yields the 10-fold of the result. Now we add
the BCD and repeat that algorithm until all decimal digits are converted. If, during
one of these operations, there occurs a carry of the result, the BCD is too big to be
converted.
The conversion of a binary to BCDs is even more complicated than that. If we convert a
16-bit-binary we can subtract 10,000, until an overflow occurs, yielding the first
digit. Then we repeat that with 1,000 to yield the second digit. And so on with 100 and
10, then the remainder is the last digit. The constants 10,000, 1,000, 100 and 10 can
be placed to the program memory storage in a wordwise organised table, like this:
DezTab:
.DW 10000, 1000, 100, 10
and can be read wordwise with the LPM instruction from the table.
An alternative is a table that holds the decimal value of each bit in
the 16-bit-binary, e.g.
.DB 0,3,2,7,6,8
.DB 0,1,6,3,8,4
.DB 0,0,8,1,9,2
.DB 0,0,4,0,9,6
.DB 0,0,2,0,4,8 ; and so on until
.DB 0,0,0,0,0,1
Then you shift the single bits of the binary left out of the registers to the carry.
If it is a one, you add the number in the table to the result by reading the numbers
from the table using LPM. This is more complicated to program and a little bit
slower than the above method.
A third method is to calculate the table value, starting with 000001, by adding this
BCD with itself, each time after you have shifted a bit from the binary to the right
and added the BCD.
To the top of that page
©2002 by http://www.avr-asm-tutorial.net