Re: Bluetooth Controlled LedDriver - A Tutorial - Part 4 added
How to code uC interrupt driven programs
In this part of the tutorial I will cover some programming tips and guidelines for programming interrupt driven programs. For a general guide to AVR assembler programming try
http://www.avr-asm-tutorial.net/avr_en/beginner/index.html and of course read the data sheet for the particular device you are using.
The previous tutorial used a very simple and reliable programming approach. It did all the work in the interrupt routine. The main code merely initialised the uC and then went to sleep waiting for the ADC to finish reading the led current. When the ADC completed, it interrupted the main loop and all the current control and button press handling was done in the ADC interrupt routine.
This project will require a more difficult coding style. In this tutorial we need to handle multiple interrupts. These are the ADC conversion complete, the timer1 interrupt for button denounce and general timing, the timer0 interrupt for timing the sending and sampling of the RS232 signals and a pin change interrupt to detect the start of reception of the next RS232 byte.
Tip 1. Make very small changes and always keep copies.
This is
the most important tip for programming. Start with the smallest program you can, say set an output low, and then get it to to compile and test it works. Then, most importantly, take a copy of this working program and re-name it version1. I actually prepend 1_ to the code file, e.g. 1_RS232.asm. I also put a comment at the top of the code saying what I changed in this version. Like
// 1 First working code, sets output low Then I make a very small change
to enhance the program and test it again, and so on. I am up to version 44 for the RS232.asm code.
One piece of software that I recommend you buy is
BeyondCompare or some other diff program. This will let you easily compare your current code to a previous version so you can see what you have done to stop it working.
A lot of programmers also use version control software like
Subversion. For these small projects with just one assemble file it is not necessary, but is very useful as your projects grow in size and complexity. I use the version numbering system while I am away from base and then put the versions into Subversion when I return.
How to Enable an Interrupt
By default all interrupts are disabled. To enable an interrupt you need to
- enable the particular interrupt you want by writing to its control register and
- enable the global interrupt flag (using SEI)
Guideline 1. Keep the interrupt code small.
While the uC is executing the code in the interrupt routine it, usually, cannot handle any other interrupts. This is because the global interrupt flag is disabled automatically on entering the interrupt routine and turned on again by the RETI instruction at the end of the routine. Level triggered interrupts that come and go while you are handling some other interrupt are not lost, other interrupts are flagged and are processed when the code returns from the RETI instruction. So long interrupt routines mean you may miss some other signals altogether or deal with them after some delay. As we have seen above, the RS232 processing depends on sending the bits and sampling the inputs within a given time. This won't happen if you code spends too long in other interrupt tasks. In general in the interrupt handler just read the data and write it somewhere for the main program to process later.
Guideline 2. Keep global disable of interrupts to as short as possible.
The CLI instruction disables all interrupts and the SEI instruction re-enables the global interrupt flag. If you need to do something that cannot be interrupted, such as moving two bytes from one place to another, then surround the code with
CLI
…
SEI
to prevent some interrupt routine from changing the registers or global values while you are moving them. This code block should be kept as small as possible.
As stated above, interrupt handlers also globally disable interrupts. Interrupts are disabled on entry to the interrupt handler automatically by the uC and re-enable them on exit (RETI). The reasons given in guideline 1 for keeping the code short also apply here.
Guideline 3. Your main program can be interrupted between any two instructions.
Following guidelines 1 and 2 most of your code should be executed with interrupts enabled. This means that your main program can be interrupted between any two instructions. Because of this, you need to restore the previous values of all Registers and global variable, that do not transfer data, before returning from Interrupt.
If the interrupt changes any of the registers or storage (SRAM) you are working with, then the main program will appear to act strangely. For example, the main program can test some value is greater then 5 and then in the next instruction, after being interrupted, the value is suddenly less than 5. This is the most common problem with interrupt programming, and the hardest to debug. So take special care to guard against this using the programming approaches outlined below.
How to Avoid Interrupt Data Corruption
There are a number of basic ways to overcome this problem of an interrupt changing a register or SRAM value unexpectedly.
There are two basic issues here.
- How to prevent interrupt routines from un-intentionally changing the values of registers and SRAM used by other parts of the program
- How to main program can safely access registers and SRAM updated by an interrupt routine.
Possible solutions to 1) Preventing un-intentional changes to register and SRAM values, are:-
- For SREG, this register MUST be saved and restored by all interrupt handlers.
- For general purpose registers, either reserving specific registers for use by specific interrupt handlers, OR by saving and restoring registers used in the interrupt handler.
- For I/O and control registers your need retain the values of the bits not being change by the interrupt.
- For SRAM, reserving specific locations for use by specific interrupt handlers.
Possible solutions for 2) Safe access to registers and SRAM that is updated by interrupts, are:-
- Disable the interrupt inside the interrupt routine and do not enable it again until the main program has finished processing the data. When the interrupt routine returns the RETI will set the global interrupt flag to enable all the other interrupts that you have not disabled.
- In the interrupt routine, save the data to an SRAM location and then in the main program copy the data to a different location for processing. If you need to copy more then one byte then surround the code with CLI and SEI to prevent any interrupt changing the data before you can save it.
- Make sure your main program has enough time to process the data before the next interrupt can occur. An example of this is processing a denounced push button. The user cannot physically move the button in less then about 0.1 sec and the debounce code will not let the button program status change faster then 10mS (about 80,000 uC clock cycles, more then 40,000 instructions) which is more then enough time for the entire main programming loop to process the button press/release.
- Do all the processing inside the interrupt routine itself. Only use this approach for small tasks that don't need to transfer any data to or from the main program. For example flashing a led.
- Use 'guard' flags to control access to registers and SRAM locations. These flags let the main program know when it is safe to access and update values that are shared by interrupt handlers. Typically the flags indicate some or all interrupts have been disabled.
Let's look at some examples of applying each of these solutions.
1) How to prevent interrupt routines from un-intentionally changing the values
1) a) Preserving and restoring the SREG register.
This is needed for EVERY interrupt handler so I wrote two small macros.
// start interrupt routine
.macro SAVE_SREG
push Temp
in Temp, SREG // save the SREG
push Temp
.ENDMACRO
// end interrupt routine
.macro RESTORE_SREG
pop Temp
out SREG, Temp // restore the SREG
pop Temp
.ENDMACRO
where Temp is defined as R15. The save macro is used at the very start of the interrupt handler. It first puts the current value of the Temp register on the stack then transfers the SREG to Temp and puts it on the stack as well. This leaves Temp free for use by the interrupt routine. At the end of the routine, just before the RETI statement, the restore macro gets the SREG value off the stack, into Temp, and restores it and then restores the value of Temp the existed before the interrupt routine was called.
1) b) Saving and Restoring General Purpose Registers
My advice is to always save and restore every register you use in the interrupt routine, except of course if you are updating a register for access from the main program or another interrupt routine. You do this by pushing the registers at the start of interrupt routine and poping the in
reverse order at the end of the interrupt routine. For example
.def RS232StatusReg = r17 // used in interrupt saved and restored
.def RS232BitCount = r18 // used in interrupt saved and restored
TIMER0_CMP_A_INT:
SAVE_SREG // save Temp and SREG
push RS232StatusReg
push RS232BitCount
...
pop RS232BitCount
pop RS232StatusReg
RESTORE_SREG // restore Temp and SREG and reti
reti // return from interrupt
If on the other hand you decide to allocate specific registers for the sole use of an interrupt routine then you need to carefully document this in your code. I do not recommend this approach because as your code grows you will run out of registers and need to push and pop them as shown above.
1) c) I/O and control registers.
I/O and control registers are shared by the whole program, so when an interrupt routine, or the main program, sets or clears a bit in one of them, you need to make sure the other bits are not changed. To do this you need to use one of two pairs of statements. Which pair you use depends on whether or not the i/o register is in the first 32, i.e. PRR to EEHRH (see the Register Summary in the datasheet).
If the i/o register is in the first 32 you can use SBI and CBI to set and clear individual bits in the register without changing the others. e.g;
sbi PORTA,SERIAL_OUT_A // Set transmit to 1 On the other hand if the register is outside the first 32 you need to load it into a temporary register first and then use SBR or CBR to set or clear the bits you want to change. Note: SBR and CBR are different from SBI and CBI. Where as SBI and CBI only need at bit number, 0 to 7, which you want to change, the SBR and CBR statements need a whole byte mask (0 to 254) to apply. e.g.
in Temp, GIFR
sbr Temp,1<<PCIF1
out GIFR,Temp ; clear pending pin change 1 int.
The 1<<PCIF1 translates to 1<<5 which shifts 1 five 5 places to the left, i.e. 0b00100000, SBR then OR's this mask with the register to set the bit, while CBR AND's the mask's complement to clear the bit
To set or clear more than one bit at a time just OR (||) the masks, i.e.
cbr Temp, (1<<CS02)|(1<<CS01)|(1<<CS00)
Tip: Instead of trying to remember if the i/o register is in the first 32, I just try using SBI, or CBI, and then if the compiler complains I replace SBI, with SBR, 1<< and add the IN and OUT statements
1) d) Reserving specific SRAM locations
SRAM is used for both stack and global volatile storage. Very early in the program you need to reserve some SRAM for use by globals by setting where the stack pointer starts (SP). e.g.
//***** Stack Pointer Setup Code *****
ldi Temp,high(RAMEND-(NO_GLOBAL_VARS+1)) ; Stack Pointer Setup
out SPH,Temp ; Stack Pointer High Byte
ldi Temp,low(RAMEND-(NO_GLOBAL_VARS+1)) ; Stack Pointer Setup
out SPL,Temp ; Stack Pointer Low Byte There are two coding statements you need to The RAMEND is defined in the include file for each AVR uC. The NO_GLOBAL_VARS is a variable I define at the top of the code. Note: the +1 to allow for global variable 0.
When I set the NO_GLOBAL_VARS, I also define a place holder for each one of them i.e.
...
.equ GLOBAL_27 = RAMEND-27
.equ GLOBAL_28 = RAMEND-28
.equ GLOBAL_29 = RAMEND-29
.equ GLOBAL_30 = RAMEND-30
Then as I use them I replace the GLOBAL_.. with the name of the variable. If I run out, I increase NO_GLOBAL_VARS and add more place holders. Your code should clearly comment the use of each of these SRAM locations.
2) How the main program can safely access Registers and SRAM updated by an Interrupt routine.
2) a) Disable the interrupt inside the interrupt routine.
A slight variation of this approach is used in the LED_CONTROL processing in the BluetoothControlledLedDriver.asm (to be posted later). In this case the interrupt routine does nothing (actually is not defined). The ADC is set for single conversions and stops when the conversion is complete. The main program looks for the conversion complete flag and processes the reading, before starting the next conversion. So there is no possibility of the ADC reading being changed while the main program is processing them. This is also an example of Polling. That is where instead of having an interrupt routine, the main program just loops check to see if something has happened. This approach is not suitable for time critical applications.
2) b) Save the data to SRAM, then copy to a different location
An example of this from RS232.asm (to be posted later) is
lds Temp, RS232_STATUS
sbrc Temp, RS232_STATUS_TX_COMPLETE
The Timer0 Compare A interrupt, and others, update the RS232_STATUS SRAM location. The main program reads this location it to a register and then tests for various statuses. Since only one statement is need to read the byte, you don't need to surround the statements with CLI, SEI.
On the other hand if we want to set/clear a bit in an SRAM location that is updated by an interrupt, you need to make sure the interrupt routine does not try the value while the main program is trying to change the value. So we surround the main program's update statement with a CLI and SEI statements to stop any interrupt handler from interrupting this group of statements.
// clear RS232_STATUS_DATA_RECEIVED
// disable of interrupts to prevent some other interrupt change the RS232_STATUS
// after we load it and before we save it. If so then that change would be lost.
cli
lds Temp, RS232_STATUS
cbr Temp, (1<<RS232_STATUS_DATA_RECEIVED)
sts RS232_STATUS, Temp
sei
2) c) Make sure your main program has enough time to process the data before the next interrupt occurs.
In the BluetoothControlledLedDriver.asm (to be posted later), a timer interrupt checks the button position and updates the SwitchChanged flag. The main loop checks this flag each time round the loop and if it is set it clears the flag and then checks the button position. The code only works if the SwitchChanged flag and button position cannot change in the time it takes the program to do an entire loop. The SwitchChanged and SW_SWDown flags can only be updated once per 10mS. This is more then enough time for for the uC to execute and entire loop of the main program. So if the flags change just after this code is executed, they will still be valid next time the program loop comes around.
sbrs TRIGGER_Flags, TRIGGER_SwitchChanged
rjmp Finished_SwitchChanged_TRIGGER_PROCESSING
// else changed clear trigger now
cbr Trigger_Flags, (1<<TRIGGER_SwitchChanged)
// else switch changed state, check switch state
// clear the trigger
sbrc SW_Flags, SW_SWDown // skip rcall if switch is up
rcall PROCESS_SWDown_TRIGGER // set level accordingly
2) d) Do all the processing inside the interrupt routine itself.
The handling of the RS232_SEND_WORD in the Timer0 Compare A interrupt handler is an example of this (see RS232.asm to be posted later). This SRAM location is not used at all by the main program. All modifications are done in the interrupt handlers. So there is no possibility that its value can be changed by the main program.
2) e) Use 'guard' flags to control access to registers and SRAM locations.
This is a very common means to controlling access to shared registers and SRAM locations. In BluetoothControlledLedDriver.asm (to be posted later), the RS232_STATUS location is used to communicate the current status of the RS232.
For example when the RS232_STATUS_BUSY is set, the link is sending or receiving and it is not safe for the main program access or update the send/receive data. So the main program can load this status byte, using method 2) b), and then check the value of RS232_STATUS_BUSY and RS232_STATUS_SEND to see if it is safe to update the data to be sent. Note: that this code depends on RS232_STATUS_SEND remaining false at the end of sending until the main program starts another send, using 2) a) above.
lds Temp, RS232_STATUS
sbrs Temp, RS232_STATUS_BUSY
rjmp SEND_ADC_READING_TRANSMIT // not busy so can send
// if busy see if sending or receiving
sbrc Temp, RS232_STATUS_SEND
rjmp END_SEND_ADC_READING // busy sending so keep trigger and try later
// else not busy sending so send this reading
// Note RS232 may be busy receiving.
// If so this transmit will just terminate the receive
// if it has not finished by the time RS232_TRANSMIT is called below
SEND_ADC_READING_TRANSMIT:
Since we have not put these tests in a CLI, SEI block, an RS232 receive could start after we tested the RS232_STATUS_BUSY flag, or as noted in the code there could be a receive already in progress. In either case, as documented in the code, the transmit will take precedence and terminate any receive that is in progress.
2) f) Update the register atomically
Updating the register atomically means doing it in such a way that an interrupt can not disrupt the update. In practice this usually means doing the update in a single statement, such as
cbr Trigger_Flags, (1<<TRIGGER_Timer1)
which clears a bit in the Trigger_Flags register in one instruction. Interrupts never interrupt a single instruction and this instruction does not change any of the other bits in the register. Some Atmel uCs also provide atomic access or update to a pair of registers which guarentees that once you read/write the first register the second will not be changed before you read/write it. For example, in the Attiny84, when you read the low byte of the ADC result register it blocks the update of the result until the high byte has been read.