Safe programming with IAR Embedded Workbench for AVR
2021-05-31 오전 7:18
This Technical Note discusses safe programming with IAR Embedded Workbench for AVR.
Protect your simultaneously accessed variables
Variables that are accessed from multiple threads must be properly marked and have adequate protection, the only exception is a variable that is always read-only.
To properly mark a variable, use the volatile keyword. This informs the compiler that the variable can be changed from other threads. The compiler will then avoid optimizing on the variable (keeping track of the variable in register for example), will not delay writes to it and be careful to access the variable only the number of times as given in the source code.
A sequence that access a variable must not be interrupted, this can be done using the __monitor keyword in interruptable code. This must be done for both write AND read sequences, otherwise you might end up reading a partially updated variable.
This is true for all variables of all sizes. Accessing a byte sized variable can be an atomic operation, but this is not guaranteed and you should not rely on it unless you always study the compiler output, ALL THE TIME. It is safer to ensure that the sequence is an atomic operation using the __monitor keyword.
Use intrinsics, not inline assembler when possible
There are mechanisms in the AVR device(s) that require precise timing and special instruction sequences. Such mechanisms are usually available in form of an intrinsic function. Available intrinsic functions are declared in the inc\inavr.h header file.
An intrinsic function looks like a normal function call, but it is really a built in function the compiler recognizes. Even though it looks like a function call in the source file, the effect is usually that the compiler generates a sequence of inline instructions.
The advantage of an intrinsic function rather than using inline assembler is that the compiler understands what is happening and can interface the sequence properly with register allocation and variables. The compiler will also be able to know how to optimize around such sequences, something the compiler is unable to do with inline assembler sequences. The end effect is that you get the desired sequence properly integrated in your code and allow the compiler to fully optimize the code.
Use assembler functions, not inline assembler when possible
Inline assembler sequences have no well defined interface to the surrounding code generated from the C code. This makes inline assembler code fragile and will possibly also becoma a maintenance problem if you upgrade the compiler in the future. The compiler also avoids optimizing functions with inline assembler to make the inline assembler code less fragile.
Inline assembler is therefore often best avoided. In many cases there are intrinsic functions available that reduce the need to write special assembler code (see above), use them instead.
If there is no suitable intrinsic function, it is often best to put the assembler code in an assembler module that is called from C. There are several benefits from this:
- The function call mechanism is well defined and will not change in future compiler releases. The __version_1 mechanism is what is normally best to use. This removes the problem of fragile code.
- Your code will probably be easier to read.
- You will allow the optimizer to work.
But, does not the function call add a lot of overhead? The answer is, it depends. Sure, there is some overhead in form of a CALL and a RET instruction and the compiler will regard some registers as scratch (destroyed by the function call).
On the other hand, the compiler will also assume that all scratch registers are destroyed by an inline assembler instruction. This leaves the overhead of the CALL and RET. In many cases, the overhead of having the optimizer turned off easily outweigths the overhead of the CALL and RET instructions.
Protect the eeprom write mechanism
The mechanism that writes to an eeprom variable must be protected as it is not reentrant. This means that if you write to eeprom from two threads (main code and interrupt for example), you will need to use the __monitor keyword to protect the write to eeprom to ensure that the sequence is not interrupted by any code that also use the eeprom write mechanism.
Protect word SFR writes
Word SFRs are accessed using a temporary byte inside the AVR. There is only one such temporary byte. If you access a word SFR from two threads (main code and an interrupt for example), you must protect the word access from interrupts, otherwise you have a very subtile bug that will not strike often. If you have the habit of turning on interrupts inside interrupts, you will have to protect word SFR accesses in such interrupts too.
Do not push locked registers
If you lock registers and put a global variable in them, consider that register as locked for this purpose in your application. Do not attempt to use this register in your own assembler code and think you can just push the variable on the stack while you lend the register.
The reason is that you are likely to access locked register variables from an interrupt. Remember, you probably put the variable in register for speed, and you are likely to access it from an interrupt, as this is where you want speed.
Also be careful if you import third party assembler code, it may use the register and preserve it on the stack will not help when your interrupt goes off.
The bottom line is, treat locked registers as hands off, all the time!
All product names are trademarks or registered trademarks of their respective owners.