15.7.1 Sharing Memory with Mainline Code
Exercise caution when modifying the same variable within a main or low-priority ISR and a high-priority ISR. Higher priority interrupts, when enabled, can interrupt a multiple instruction sequence and yield unexpected results when a low-priority function has created a multiple instruction Read-Modify-Write sequence accessing that same variable. Therefore, embedded systems must implement an “atomic” operation to ensure that the intervening high-priority ISR will not write to the variable from which the low-priority ISR has just read, but not yet completed its write.
An atomic operation is one that cannot be broken down into its constituent parts – it cannot be interrupted. Not all C expressions translate into an atomic operation. On dsPIC DSC devices, these expressions mainly fall into the following categories: 32-bit expressions, floating point arithmetic, division, operations on multi-bit bit-fields, and fixed point operations. Other factors will determine whether or not an atomic operation will be generated, such as memory model settings, optimization level and resource availability. In other words, C does not guarantee atomicity of operations.
Consider the general expression:
foo = bar op baz;
The operator (op
) may or may not be atomic, based on
the architecture of the device. In any event, the compiler may not be able to generate
the atomic operation in all instances, depending on factors that may include the
following:
- availability of an appropriate atomic machine instruction
- resource availability - special registers or other constraints
- optimization level, and other options that affect data/code placement
Without knowledge of the architecture, it is reasonable to assume that the general expression requires two reads, one for each operand and one write to store the result. Several difficulties may arise in the presence of interrupt sequences, depending on the particular application.
Development Issues
Here are some examples of the issues that should be considered:
bar Must Match baz
When it is required that bar
and
baz
match (i.e., are updated synchronously with each other),
there is a possible hazard if either bar
or baz
can be updated within a higher priority interrupt expression. Here are some sample
flow sequences:
- Safe:
read
bar
read
baz
perform operation
write back result tofoo
- Unsafe:
read bar
interrupt modifies
baz
read
baz
perform operation
write back result tofoo
- Safe:
read
bar
read
baz
interrupt modifies
bar
orbaz
perform operation
write back result tofoo
The first is safe because any interrupt falls outside the boundaries
of the expression. The second is unsafe because the application demands that
bar
and baz
be updated synchronously with each
other. The third is probably safe; foo
will possibly have an old
value, but the value will be consistent with the data that was available at the
start of the expression.
Type of foo, bar and baz
Another variation depends upon the type of foo
,
bar
and baz
. The operations “read bar,” “read
baz,” or “write back result to foo,” may not be atomic depending upon the
architecture of the target processor. For example, dsPIC DSC devices can read or
write an 8-bit, 16-bit, or 32-bit quantity in 1 (atomic) instruction. But a 32-bit
quantity may require two instructions depending upon instruction selection (which in
turn will depend upon optimization and memory model settings). Assume that the types
are long
and the compiler is unable to choose atomic operations for
accessing the data. Then the access becomes:
read lsw bar
read msw bar
read lsw baz
read msw baz
perform operation (on lsw and on msw)
perform operation
write back lsw result to foo
write back msw result to foo
Now there are more possibilities for an update of
bar
or baz
to cause unexpected data.
Bit-fields
A third cause for concern are bit-fields. C allows memory to be
allocated at the bit level, but does not define any bit operations. In the purest
sense, any operation on a bit will be treated as an operation on the underlying type
of the bit-field and will usually require some operations to extract the field from
bar
and baz
or to insert the field into
foo
. The important consideration to note is that (again
depending upon instruction architecture, optimization levels and memory settings) an
interrupted routine that writes to any portion of the bit-field where
foo
resides may be corruptible. This is particularly apparent
in the case where one of the operands is also the destination.
The dsPIC DSC instruction set can operate on 1 bit atomically. The compiler may select these instructions depending upon optimization level, memory settings and resource availability.
Cached Memory Values in Registers
Finally, the compiler may choose to cache memory values in
registers. These are often referred to as register variables and are particularly
prone to interrupt corruption, even when an operation involving the variable is not
being interrupted. Ensure that memory resources shared between an ISR and an
interruptible function are designated as volatile
. This will inform
the compiler that the memory location may be updated out-of-line from the serial
code sequence. This will not protect against the effect of non-atomic operations,
but is never-the-less important.
Development Solutions
Here are some strategies to remove potential hazards:
- Design the software system such that the conflicting event cannot occur. Do not share memory between ISRs and other functions. Make ISRs as simple as possible and move the real work to main code.
- Use care when sharing memory and, if possible, avoid sharing bit-fields which contain multiple bits.
- Protect non-atomic updates of shared memory from interrupts as you
would protect critical sections of code. The following macro can be used for this
purpose:
#define INTERRUPT_PROTECT(x) { \ char saved_ipl; \ \ SET_AND_SAVE_CPU_IPL(saved_ipl,7); \ x; \ RESTORE_CPU_IPL(saved_ipl); } (void) 0;
This macro disables interrupts by increasing the current priority level to 7, performing the desired statement and then restoring the previous priority level.
Application Example
The following example highlights some of the points discussed in this section:
void __attribute__((interrupt))
HigherPriorityInterrupt(void) {
/* User Code Here */
LATGbits.LATG15 = 1; /* Set LATG bit 15 */
IPC0bits.INT0IP = 2; /* Set Interrupt 0
priority (multiple
bits involved) to 2 */
}
int main(void) {
/* More User Code */
LATGbits.LATG10 ^= 1; /* Potential HAZARD -
First reads LATG into a W reg,
implements XOR operation,
then writes result to LATG */
LATG = 0x1238; /* No problem, this is a write
only assignment operation */
LATGbits.LATG5 = 1; /* No problem likely,
this is an assignment of a
single bit and will use a single
instruction bit set operation */
LATGbits.LATG2 = 0; /* No problem likely,
single instruction bit clear
operation probably used */
LATG += 0x0001; /* Potential HAZARD -
First reads LATG into a W reg,
implements add operation,
then writes result to LATG */
IPC0bits.T1IP = 5; /* HAZARD -
Assigning a multiple bitfield
can generate a multiple
instruction sequence */
}
A statement can be protected from interrupt using the
INTERRUPT_PROTECT
macro provided above. For this example:
INTERRUPT_PROTECT(LATGbits.LATG15 ^= 1); /* Not interruptible by
level 1-7 interrupt
requests and safe
at any optimization
level */