If you tell any web or mobile developer that you've been spending your days and nights reading and writing embedded C, they'll probably think you're courageous for embarking on the journey or they might give you a hug and tell you the name of this "really great therapist" they know.
Well, I'll take a high five, a hug, or a combination of both from you and your therapist.
While Embedded development is both challenging and exciting, it requires a different approach to many things. The constraints are different: memory, speed, stack size, heap size, etc. The toolchain is different: compilation, linking, memory maps, debugging, physical hardware integration. Things rarely thought about before become a daily consideration.
Even with these differences, I'm finding that many of the conventions, practices, and values I've acquired over the years working at a higher level still apply. Isolating sections of code has always been a catalyst for my learning; then each one can be understood as a unit, rather than just a giant bunch of highly suspect code.
C code should, by default, be under suspicion. Its easily grotesque and cryptic syntax is a place where issues can hide in the open. In the code I'm currently working with there are either too few function boundaries or they're poorly named. Either way, I've spent far too much re-reading code that I just read a few days ago, figuring it out, again. A few decently named functions would have helped quicken the understanding and save that repeated time and effort.
There are lots of ways to improve C code and I'd like to share some with you. I'm by no means an expert, but I do have a fresh set of eyes. So here's something I learned higher up the stack that I've been effectively applying when writing embedded C code: small, well-named, functions.
Small, well-named functions
Having small, well-named functions is better than having fewer functions or, heaven forbid, one monstrous function. It's better because when you're working with smaller, bite-size chunks of code it's easier to understand, maneuver around, refactor, extract, test, and delete.
Because smaller functions tend to be more focused it also makes the job of naming them a bit easier. We get to avoid attempting to name aFunctionThatDoesThisAndThatAndAnotherThingEtc which then often leads to aCrypticallyOrPoorlyNamedFunctionThatDoesStuff. I much prefer the simpler approach of clearly naming aFunctionThatDoesOneThing.
Once I have a few well named functions I find myself wishing all my other functions were equally as well named. There's something about the allure of clear communicative code that is both enticing and challenging at the same time. It's hard to beat the feeling that accompanies pulling out 20 lines of code from some init or processEvent function and giving it an identity.
For example, consider the function MyProgram_Init, which, has down in its bowels, intermingled with code doing all sorts of things, a section of code that sets Bluetooth Smart advertising data. It's not clear unless you tediously comb through the code.
Extract it.
Name it.
SetAdvertisingData.
Nicely done, a function that we can both form some ideas about without even having to dive into its internals.
Not to mention, once there are well-named functions the functions that call them become more self-documenting, and this recursively applies as you go up the function call stack.
In four days, four weeks, or four months you may find yourself having to revisit this code and the reduced cognitive overhead may allow you to ramp back up more quickly. Or, if it's not you, it may help the next person who touches the code grok it with much less time and effort.
There are two sides to this coin: readability and stack size. Embedded devices have limited amounts of space and stack sizes are typically fixed. Overflowing the stack will not have a good outcome. Every time a function is called all of those automatic variables – temporary local variables, temporary results of expressions, processor state during interrupts, processor registers that need to be restored, etc. – are put on the stack. So stack usage is directly related to your function call-tree.
There are ways around stack size issues such as the compiler optimization for inlining functions, but there are usually some constraints around when and what can be inlined. Inlining functions also increases code size which may or may not be an issue based on the amount of storage space you're working with.
So far I haven't had any issues running out of stack so I'm going to continue organizing my code primarily for the reader (rather than the compiler/linker) while keeping in mind the very real constraints that exist. Compilers are much better at mangling code than I am anyways, despite my best efforts.
In the end, I want my future self to be proud of the code my present self is writing even though he will indubitably know that he can always do better than his past self. Now that's something to share with your developer therapist.
Happy coding.