The key to embedded software design is to identify the processes and their time constraints. How to implement these processes is the next step but coding style / modularisation are at the maintenance and verification side of the spectrum rather than at the functional side.
The processes are probably modules or modular themselves.
If one thing requires a 400ns response time, then right there is your boundary which sets that bit of code aside from others. What point would there be, say, in coupling everything to the same 400ns timing constraint?
Do other bits of code provide to this 400ns process or consume from it?
You comment on unit testing, might miss the point.
"This C files works as intended".
"Prove it."
"Ok, let me fire up the rest of the project."
"Waiting...."
"Oh, sorry the UART isn't setup, can't test it."
That or...
"It worked fine with that first UART periph we used, why doesn't the test pass now? Which bit is broken?"
The real jewel in the crown of unit testing with proper isolation is when you "tweak that one thing" or do that "one merge" or "one bit of refactor", you at least get a "signal of confidence" when the unit tests "STILL pass".
There are those who argue if you can make ANY change at all to a code base and not have at least one test fail, you haven't got enough testing.
There are even "Dynamic unit tests" these days. A process where by an application forces mutations on your test values and logs if the test fails or passes with "junk data". "Mutative testing".
I'll give you a few clear examples of coupling and how unit testing pulls them out when you don't normally even see them.
You have a C module which calls an RTC to get a binary-decimal value for "now". It might then decode that into the values it wants, like epoch. It might then compare it to a "last watermark" and if more than 1 minuted has passed and if it's a weekday, start a process running.
Sounds fair right? How do you test this in isolation? "Invoke it and look at the output?". Okay, so how do you verifiy it's working as intended?
The options are to try and synchronise your harness clock with the DUT and wait for 1 minute to check it started the process. On a weekday... and how do you set the last watermark run it twice?
OR
and this one is valid: "Mock" the RTC. Have the "fake RTC" provide that "getNow" functionality, like the real RTC. Provide a fixed test date or array of troubling dates which cause bugs before - which is the professional touch.
It might be better to provide a "setNow" feature to the module and not have it go and get it itself. The later plumbing is about 1 line. Keeps the RTC gubbins, including that decode to epoch, with the RTC gubbins and away from "business logic".
You electronics folks use "Mocks" all the time and don't call them that.
Consider testing a tranmission line. It is modelled as a separate component on it's own for a reason. When you are testing something which will use a transmission line, you don't give it the real one, do you? You give it the test "mock" transmission line.
When you want to create a latch structure which must respond to CPU 'edges' and provide edges with the correct timing. Do you hook it up to a real CPU while in dev? Or do you use an ARB or Logic generator to produce the edges and a scope or LA to capture the response. If your quality and test requirements require it you have a bench model of that setup run on every board... no?
Of course it's only ONE form of testing, but arguably the first line of defense. It also teases out the problems with code when it can't be tested insolation it means it coupled to it. Two ore more become one. They aren't really separate modules at all. You can't have one without the other etc.
In isolation these all sound trival, but in aggregate they kill code bases due to untracable and undocumentable behaviours later.
In the professional side, you literally cannot submit code without unit tests. It will be rejected instantly, "Computer says no" for a start but also the team will raise it if the pipeline does not.