Best practices and industry standards develop and evolve over time but they represent a snap shot of guiding wisdom. Best practices can be slow to evolve and often become entrenched despite technological advances that void the previously known best practice. The use of C language features within the embedded systems space has suffered the same fate. Many best practices have their roots in the 80’s and 90’s when compilers were quirky and microcontrollers were truly resource constrained. Compilers and microcontrollers have come a long way since then and with them many “forbidden” features and design techniques just might not be so anymore.
Feature #1 – float
The use of float in embedded systems has for a very long time been strongly opposed. Float traditionally has a number of potential sources of problems. First, microcontrollers don’t have floating point units. For the most part this statement remains true today for the lowest end microcontrollers but the cost to include a floating-point unit (FPU) has come down drastically to the point that mid range and general microcontrollers are starting to include an FPU. Advances in microcontroller technology also now include hardware acceleration for mathematical functions, which even without an FPU can aid in faster calculations.
Second, the use of float without an FPU requires the compiler to pull in software libraries that are bulky and slow. Compiler technology has drastically improved since the late 20th century and performing a floating-point calculation even on an 8-bit microcontroller in software is optimized to the point of negligible. Don’t believe me? Try it! Results will vary and may even align with conventional wisdom but the fact is technology is changing and developers need to change with it.
Feature #2 – malloc
Malloc allows a developer to dynamically allocate memory during program execution and is potentially a very dangerous tool when improperly used within an embedded system. Traditionally the use of malloc has been completely prohibited in resource-constrained systems and for good reasons. What happens when the heap becomes fragmented? How does a developer handle a failure to allocate memory? What about memory leaks? These are difficult programs that require code space and horsepower to handle which the traditional microcontroller can’t handle.
Microcontrollers are no longer “traditional”. A low cost microcontroller in 2015 may have clock speeds running in excess of 200 MHz, more than 1 MB of flash space and RAM upwards of 64 KB (as high as 256 MB). Even in a system that isn’t quite this beefy, the tools for properly implementing and using malloc are available. The application may very well warrant it as well so it shouldn’t from the start be thrown out of considerations just because of outdated best practices.
Feature #3 – printf
In general, the use of C library functions within an embedded system is considered to be poor practice. Most of the C library is not reentrant, is usually bulky and slow to execute (or is it?) or is implemented in such a way that it blocks execution until complete. The use of printf falls into many of these categories and has been shunned from use in embedded systems. Embedded developers often feel guilty for using printf or in conversations will say, “I know I shouldn’t have but I added printf …”.
As previously mentioned a lot has changed and between compiler optimizations and hardware advancements the use of printf is the guilty pleasure it used to be. The function while considered “large” takes up little code space given the typical 32 kB flash space. Typical implementations have printf act as a blocking function, which can affect real-time response and hog a potential shared resource. Response issues can easily be overcame by linking printf to a circular buffer and interrupt driver transmit driver that allows program execution to continue while the driver does the necessary work. The circular buffer helps keep messages ordered as first come first serve.
Feature #4 – memset
Most C features that involve memory manipulation or dynamic memory end up on the forbidden feature list. The reason is pretty obvious, many developers and teams have gotten burned in the past using such features. Rather than get burned again they are moved into the never use category. When a feature such as malloc or memset make the most sense though they shouldn’t be shied away from. Instead developers should make sure that they completely understand how to use the feature, what precautions are needed for proper use, how to recover in the event the worst case happens and then of course proper testing to ensure that everything goes according to plan.
Feature #5 – C bit fields
One concept whose use is not rooted in the advancement of compiler technology or hardware advances is the use of features that are ambiguously defined in the C standard. A great example of such a feature is a bit field. The biggest problem with using bit fields is that there are usually portability issues. A simple example is the fact bit ordering isn’t standard and the bits can often be rearranged by the compiler to what it decides is the most efficient means of implementation. There are also issues with padded bytes being added to the overall structure.
The general rule of thumb has been to avoid the use of bit fields but there are plenty of instances where they make sense and even where the portability issues don’t matter. A great example of using bit fields is for creating a structure that is used to create a configuration table for initializing a driver or application. With the initialization written to read the value of the bit, reordering or even padded bytes won’t be an issue.
Standards and best practices are designed to help prevent developers from shooting themselves in the foot, but they are just that, best practices. A standard may say that the use of a C feature is forbidden but the fact of the matter is that it is up to the developer in his or her own unique situation to determine whether conventional wisdom applies. Challenging our preconceptions and ensuring we understand why those best practices are in place is just as important as following them. Don’t write-off features without understanding their application, the risks and the rewards. Developers need to understand their timing, performance and size constraints when considering the use of these “forbidden” features.
What other features have you encountered that work in todays development environment but traditionally been considered forbidden?
Memset: “Most C features that involve memory manipulation or dynamic memory end up on the forbidden feature list. The reason is pretty obvious,,, ”
It is not obvious to me why memset should be a particular problem. Ok maybe if dynamic memory management is involved. Otherwise it is similar to writing to an array. You want to be sure nobody else is writing – but that’s not special to memset.
Thanks for clarifications.
Fairly trivial but something that is now picked up by C compilers which was not picked up in the past.
Because of the common mistake of using, say
‘ if (x = const)’ instead of ‘ if (x== const)’
It was recommended to use ‘if (const == x)’ so that the mistake – ‘if (const = x)’ would be picked-up when compiling.
However, currently C compilers will pick up the error & flag a warning for
‘if (x= const)’, so it’s no longer relevant which value comes first.
1) Floats are useful in many control applications. As long as you are careful with:
– Not comparing floats for equality (i.e. you should always do >= or <= and never ==).
– Making sure you don't add a small number to a large one (you lose resolution real quick).
As you indicated more MCUs are equipped with an FPU which makes life a lot easier. I think that using floats is less prone to errors than scaled arithmetic.
2) malloc() is fine if you don't free(). I still believe you should avoid malloc() because however much RAM you have, fragmentation will happen (most likely in the field, not the lab).
3) I don't like to use printf() because it's typically bulky, non-deterministic and affects the timing of embedded systems. If used in an RTOS environment, you have to be careful about reentrancy. Some compilers implement Thread Local Storage but that needs to be considered by the RTOS during context switches.
4) memset() is fine but very compiler specific since its implementation is tool vendor specific. Compiler vendors typically optimize memset(), though.
5) Bitfields, I don't recall ever using those. Maybe because years ago it was suggested that they are not portable and you need to be carful with read-modify-write.