Some Ancient History and the nitty gritty.
Glad to hear you are taking the journey.
ditto
It will give you some knowledge of what goes on under the hood.
Just about any compiler that will generate assembly code will let you see what is under-the-hood. I definite recommend looking at the generated assembly code.
Way back in the 1970s I bought a copy of UCSD Pascal. I was in the navy at the time stationed in San Diego at Ballast Point. Donald Knuth was teaching at UCSD then. I saw him in the halls. I didn't really know who he was then except for yet another professor. I got copy 452 of UCSD Pascal and as one of the first outside the university to get my copy running. Some assembly was required
UCSD Pascal was one of the first programming environments that would quickly put the cursor where you made a lexical mistake. Logic mistakes you had to find on your own.
The key thing I learned was how a modern compiler generated code with stack frames etc. Pascal was not object oriented but it did stress being organize, block programming, and eliminating gotos that were common is basic programming.
At a computer club meeting, yes there were computer clubs back then, I showed of my UCSD Pascal and immediately got an offer for twice what I was making in the navy back then but I was stuck
I think a second point I would like to make is that low level drivers require knowing assembly language which is kind of like programming in STL on a S7-300. One had to know how to use a linker and locator back then so the code would execute at a specific spot in the computer memory.
I have written a couple of real time kernels. I can't call them operating systems because they weren't. An embedded project often will have 5 or 6 tasks the kernel needs to manage. There are basic functions that are necessary.
Wait flag. This call is made when waiting for something. Waiting for a character interrupt from a serial uart was common back then. The wait flag routine would would stop the running task and call the task switcher. The task was put on a priority queue.
Set flag. This call is usually made from an interrupt routine. For example a set flag would be called when a character was received. This would allow the routine that called the wait flag to run by putting it on the run queue.
delay. This call is similar to the wait flag but it also has a delay parameter. The task stops running and is put on the delay queue.
dec_dealy. Decrement delay is called by the clock interrupt. Any tasks that have been running long enough are put on run queue.
mx_access. mx is short for mutual exclusion. mx_access is necessary to keep two task from accessing the same resource at the same time. A resources is assigned a token and this is used in the mx_access call. Any other task that wants to access this same resource must also call mx_access. If the resource is already in use the task will stop running and put on the waiting queue for that resource.
mx_release. mx_release is called when a task is done with the resource. The next device that is waiting for that resource is put on the run queue.
BTW, I didn't mention that when tasks are put on the run queue it is done by priority.
Notice I didn't cover memory allocation. This is because all memory is normally fixed in embedded applications.
Haven't you wondered how the internals of a PLC or motion controller work?
There is a lot more to it then just knowing C.