I have the the following situation: An STM32L476RG (ARM Cortex M4F @ 80MHz, 1MB Flash, 128kB RAM) parses some string data coming in over UART. The project uses mbed-os. The parsing of the data is done in an interrupt-based manner, meaning an ISR is in place which reads the just received character and has some stateful parsing logic to extract data from it. While doing so, it modifies an working-object which gradually gets filled up with the parsed values.
When the parsing process is done (all values filled), the working copy is saved into another global object of the same type. This is so that the application has an object with valid values and doesn't have to access the working copy of the ISR-based parser, which might change anytime. The structure in question is about 204 bytes big.
Code looks like (high level)
struct MeterState {
double valueA;
double valueB;
double valueC;
char id[64];
//more..
}
volatile MeterState isrWorkingCopy;
volatile MeterState lastValue;
/* called in an ISR context by mbed-os whenever something is received on the serial */
static void SerialParserHandler() {
uint8_t data = (uint8_t) meterSerial.getc();
parse_data(data, &isrWorkingCopy);
//check parsing finished..
if(parsingFinished) {
//save finished object
lastValue = isrWorkingCopy;
}
}
/* called by application to get last valid data */
void MeterState GetLastValue() {
return lastValue; //copy structure to as return value to stack
}
However, here comes the crux of the problem. Due to length of the object and given some certain timing, it may be possible that:
- Appcode calls
GetLastValue()
- Function starts copying the huge structure to the stack of the caller.
- Mid-term gets interrupted by the ISR
- ISR overwrites
lastValue
with new data when parsing is finished GetLastValue()
is resumed after ISR- Resulting returned value is half the old half the new value
See e.g. compiler output for GetLastValue()
, it is implemented by a memcpy()
:
--
08015f3c <_ZN13ElectricMeter16GetLastValueEv>:
8015f3c: b508 push {r3, lr}
8015f3e: 22d0 movs r2, #208 ; 0xd0
8015f40: 4901 ldr r1, [pc, #4] ; (8015f48 <_ZN13ElectricMeter16GetLastValueEv+0xc>)
8015f42: f004 fa5f bl 801a404 <memcpy>
8015f46: bd08 pop {r3, pc}
8015f48: 20001d38 .word 0x20001d38
Basically overwriting and getting the value is non-atomic. I'm not quiet seeing how it's possible to make it atomic so that the above scenario doesn't occur anymore.
I cannot just use a Mutex-based locking mechanism to atomatically exchange the lastValue
because I cannot lock a mutex inside an ISR. The ISR must be non-blocking.
I also thought about temporarily disabling interrupts around the getter method as such
MeterState GetLastValueSafe() {
__disable_irq();
MeterState s = GetLastValue();
__enable_irq();
return s;
}
However, doesn't this have the possibility (as small as it may be) that the parser misses a character (which might screw up parsing and constitute a lost data value)? If the IRQs are disabled and the handler doesn't get invoked, the character is basically lost, as I understand.
Is there any sane way to solve this general problem of ISR concurrency with large structures?
lastValue
can be modified during a time in which a memory dereference is done. \$\endgroup\$