Файл: Beginers introduction to the Assebly Language of ATMEL-AVR Microprocessors (Gerhard Schmidt,2003, англ).pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 13.06.2025
Просмотров: 646
Скачиваний: 1
Avr-Asm-Tutorial |
23 |
http://www.avr-asm-tutorial.net |
Interrupts and program execution
Very often we have to react on hardware conditions or other events. An example is a change on an input pin. You can program such a reaction by writing a loop, asking whether a change on the pin has occurred. This method is called polling, its like a bee running around in circles searching for new flowers. If there are no other things to do and reaction time does not matter, you can do this with the processor. If you have to detect short pulses of less than a µs duration this method is useless. In that case you need to program an interrupt.
An interrupt is triggered by some hardware conditions. The condition has to be enabled first, all hardware interrupts are disabled at reset time by default. The respective port bits enabling the component's interrupt ability are set first. The processor has a bit in its status register enabling him to respond to the interrupt of all components, the Interrupt Enable Flag. Enabling the general response to interrupts requires the following command:
SEI ; Set Int Enable Bit
If the interrupting condition occurs, e.g. a change on the port bit, the processor pushes the actual program counter to the stack (which must be enabled first! See initiation of the stackpointer in the Stack section of the SRAM description). Without that the processor wouldn't be able to return back to the location, where the interrupt occurred (which could be any time and anywhere within program execution). After that, processing jumps to the predefined location, the interrupt vector, and executes the instructions there. Usually the instruction there is a JUMP instruction to the interrupt service routine, located somewhere in the code. The interrupt vector is a processor-specific location and depending from the hardware component and the condition that leads to the interrupt. The more hardware components and the more conditions, the more vectors. The different vectors for some of the AVR types are listed in the following table. (The first vector isn't an interrupt but the reset vector, performing no stack operation!)
Name |
Interrupt Vector Adress |
Triggered by |
||
2313 |
2323 |
8515 |
||
RESET |
0000 |
0000 |
0000 |
Hardware Reset, Power-On-Reset, Watchdog Reset |
INT0 |
0001 |
0001 |
0001 |
Level change on the external INT0 pin |
INT1 |
0002 |
- |
0002 |
Level change on the external INT1 pin |
TIMER1CAPT |
0003 |
- |
0003 |
Capture event on Timer/Counter 1 |
TIMER1COMPA |
- |
- |
0004 |
Timer/Counter 1 = Compare value A |
TIMER1 COMPB |
- |
- |
0005 |
Timer/Counter 1 = Compare value B |
TIMER1 COMP1 |
0004 |
- |
- |
Timer/Counter 1 = Compare value 1 |
TIMER1 OVF |
0005 |
- |
0006 |
Timer/Counter 1 Overflow |
TIMER0 OVF |
0006 |
0002 |
0007 |
Timer/Counter 0 Overflow |
SPI STC |
- |
- |
0008 |
Serial Transmit Complete |
UART TX |
0007 |
- |
0009 |
UART char in receive buffer available |
UART UDRE |
0008 |
- |
000A |
UART transmitter ran empty |
UART TX |
0009 |
- |
000B |
UART All Sent |
ANA_COMP |
- |
- |
000C |
Analog Comparator |
Note that the capability to react to events is very different for the different types. The addresses are sequential, but not identical for different types. Consult the data sheet for each AVR type.
The higher a vector in the list the higher is its priority. If two or more components have an interrupt condition pending at the same time, the upmost vector with the lower vector address wins. The lower int has to wait until the upper int was served. To disable lower ints from interrupting during the execution of its service routine the first executed int disables the processor's I-flag. The service routine must re-enable this flag after it is done with its job.
For re-setting the I status bit there are two ways. The service routine can end with the command:
RETI
This return from the int routine restores the I-bit after the return address has been loaded to the program counter.
The second way is to enable the I-bit by the instruction
SEI ; Set Interrupt Enabled RET ; Return
This is not the same as the RETI, because subsequent interrupts are already enabled before the program
Avr-Asm-Tutorial |
24 |
http://www.avr-asm-tutorial.net |
counter is re-loaded with the return address. If another int is pending, its execution is already starting before the return address is popped from the stack. Two or more nested addresses remain on the stack. No bug is to be expected, but it is an unnecessary risk doing that. So just use the RETI instruction to avoid this unnecessary flow to the stack.
An Int-vector can only hold a relative jump instruction to the service routine. If a certain interrupt is not used or undefined we can just put a RETI instruction there, in case a false int happens. In a few cases it is absolutely necessary to react to these false ints. That is the case if the execution of the respective service routine does not automatically reset the interrupt condition flag of the peripheral. In that case a simple RETI would reset in never-ending interrupts. This is the case with some of the UART interrupts.
As, after an interrupt is under service, further execution of lower-priority ints is blocked, all int service routines should be as short as possible. If you need to have a longer routine to serve the int, use one of the two following methods. The first is to allow ints by SEI within the service routine, whenever you're done with the most urgent tasks. Not very clever. More convenient is to perform the urgent tasks, setting a flag somewhere in a register for the slower reactions and return from the int immediately.
A very serious rule for int service routines is: First instruction is always to save the status register on the stack, before you use instructions that might change flags in the status register. The interrupted main program might just be in a state using the flag for a branch decision, and the int would just change that flag to another state. Funny things would happen from time to time. The last instruction before the RETI therefore is to pop the status register content from the stack and restore its original content.
For the same reason all used registers in a service routine should either be exclusively reserved for that purpose or saved on stack and restored at the end of the service routine. Never change the content of a register within an int service routine that is used somewhere else in the normal program without restoring it.
Because of these basic requirements a more sophisticated example for an interrupt service routine here.
.CSEG ; Code-Segment starts here
.ORG 0000 ; Address is zero
RJMP Start ; The reset-vector on Address 0000
RJMP IService ; 0001: first Int-Vektor, INT0 service routine [...] here other vectors
Start: ; Here the main program starts
[...] here is enough space for defining the stack and other things
IService: ; Here we start with the Interrupt-Service-Routine PUSH R16 ; save a register to stack
IN R16,SREG ; read status register PUSH R16 ; and put on stack
[...] Here the Int-Service-Routine does something and uses R16 POP R16 ; get previous flag register from stack
OUT SREG,R16 ; restore old status
POP R16 ; get previous content of R16 from the stack RETI ; and return from int
Looks a little bit complicated, but is a prerequisite for using ints without producing serious bugs. Skip PUSH R16 and POP R16 if you can afford reserving the register for exclusive use in the service routine. As an interrupt service routine cannot be interrupted (unless you allow interrupts within the routine), all different int service routines can use the same register.
That's it for the beginner. There are some other things with ints, but this is enough to start with, and not to confuse you.
Avr-Asm-Tutorial |
25 |
http://www.avr-asm-tutorial.net |
Calculations
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.
Number systems in assembler
The following formats of numbers are common in assembler:
•Positive whole numbers (Bytes, Words, etc.),
•Signed whole numbers (Integers),
•Binary Coded Digits, BCD,
•Packed BCDs,
•ASCII-formatted numbers.
Positive whole numbers (bytes, words, etc.)
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.
Signed numbers (integers)
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 1000.0001 but as 1111.1111 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 (1111.1111) and +1 (0000.0001) yields exactly zero, if you forget the overflow that occurs during that operation (the nineth bit).
In one byte the biggest integer 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.
Binary Coded Digits, BCD
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.:
Avr-Asm-Tutorial |
26 |
http://www.avr-asm-tutorial.net |
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.
Packed BCDs
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.
Numbers in ASCIIformat
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 character systems. This 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.
Bit manipulations
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 R16,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:
Avr-Asm-Tutorial |
27 |
http://www.avr-asm-tutorial.net |
OR R1,R2
Back from an ASCII character to a BCD is as easy. The instruction
ANDI R1,0x0F
isolates the lower four bits (= the lower nibble). Note that ORI and ANDI are 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 (subtracting 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 with a copy of bit 0 in register R1. The T-bit can be set or cleared, and its content can be copied to any bit in any register:
CLT ; clear T-bit, or SET ; set T-bit, or
BST R2,2 ; copy T-bit to register R2, bit 2
Shift and rotate
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 out 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“ ASR leaves bit 7 untouched and shifts the 7 lower bits, inserting a zero into bit location 6.
Avr-Asm-Tutorial |
28 |
http://www.avr-asm-tutorial.net |
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 instruction exchanges the upper and lower nibble. Note that the upper nibble's content will be different after applying these two methods.
Adding, subtracting and comparing
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, handle 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 bigger 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 Carry 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,