You've fortunately been able to validate the issues I was concerned about -- pre-emption. So we both know the source of the problem, now. No question.
There were two huge elephants in the room that I saw.
Your code written like this:
encoderState.isReady = (receivedValue & 0b1000) >> 3;
encoderState.isMHI = (receivedValue & 0b100 ) >> 2;
encoderState.isMLO = (receivedValue & 0b10 ) >> 1;
And your blind use of a library routine.
In the first case, ask yourself "What happens if the operating system's heart-beat timer fires between the first and second line or perhaps, worse still, in the middle of some of the assembly language statements that are processing the semantics of the first line? Will other code that uses this structure and returned to running by the O/S be able to make sense of it, if it is only partially updated or otherwise in the middle of getting things written out?"
The same questions could also be applied to the library routine, which may be in the middle of operating the hardware peripheral. For example, there may be some critical timing involved there. Or? Who knows?
Pre-emption isn't something to take lightly. (Neither is the ignorant use of floating-point. See this paper for some thoughts there.)
Most people will have had some experience using workstation level operating systems. But these have professional drivers written for them for the hardware that they operate. Experience with tasks in C#, F#, or VB.NET ill-prepare programmers for what takes place when having to deal with hardware peripherals in the face of pre-emption. Though if they work a lot in the area of multi-threaded apartments under Windows, for example, they will have learned much of what's needed to be known. (How to handle multiple CPUs that run literally at the same exact time and can access the exact same memory location effectively at the same moment, for example.) But without that experience, a lot of this just flies right over their heads.
I personally think that the better way to learn is to dive in and write a multi-threaded O/S. I'd say to first start with a simple co-operative system that uses a switch call to allow a thread to release control at a convenient place. It also means you can use library code without worrying as the only way a cooperative mode operating system can switch tasks is when one of them asks for it, which is always after any library code has been completed. This is very easy because all you need is a structure to hold certain register values that need to be preserved using a very short assembly routine to do it. But it forces you to learn about the language's specifications for registers that must be preserved. (The so-called preserve registers, which won't be all of them but just some, as a function call in C for example expects that some registers will scratched by making the call.) Once you understand this, and I've taught it to programmers who were able to implement their own in less than a day's time, then you can move on to consider pre-emption details. (Of course, to teach it in less than a day the programmer must already be experienced with C and assembly and mixing the two.)
For learning pre-emption, I'd recommend Doug Comer's 'red book' on XINU. This is his first edition and it includes several important ideas that he later removed from following editions to make room for other topics. For example, he introduces the idea of memory marking and this is something important to gather up. (Gone from later editions, as he expected that anyone reading them would download the source and find the code handling memory marking and read it on their own.) Another important idea to get is the delta queue.
Writing a pre-emptive O/S requires more time. But it is still doable in a week's time if you keep the ideas simple enough and don't go crazy. A semaphore queue, a sleep queue, and the ready queue, for example. But this will sensitize you to writing libraries that can be pre-empted and to ideas related to the so-called upper and lower half code used for hardware drivers and their events and servicing needs.
There's nothing like putting in a week or two on the topic making something that actually works to deepen these things into your bones. Also, get in the practice of looking at the generated machine code/assembly code that your compiler produces. Not all the time, obviously. But every so often. Do it enough so that if you are looking at some section of C you can predict what will be generated with some accuracy. Test yourself from time to time. And learn what volatile
does and does not do for you, as it applies to local registers and local stack stores as well as static lifetime structures of various incarnations, and heap space. Sometimes, it also helps to understand how the linker works and how you can alter its behavior, when needed. But that's more 'down the road, a bit.' But you should keep the idea in view and work on it when you get time. You should also understand the basic program model used by compilers and linkers. This includes execute-only, read-and-execute, initialized read-write, uninitialized read-write, heap, stack; and the additional sections required by Harvard vs von Neumann.
(receivedValue >> 3) & 1
(shift then mask) rather than mask then shift. This is a cleaner way to get bits into a boolean variable since you're always ANDint with 1. \$\endgroup\$