This article will take a look at the issues involved in using C in the development of systems with safety-critical functionality.
Despite the fact that the language is full of undefined behavior, hardware dependencies and other pitfalls, it is still a widely used and popular language in the field of safety-critical development. With some forethought and planning, we can turn a potential problem into an advantage.
Back in 1991, the magazine Developer’s Insight published an entertaining article titled How to Shoot Yourself In the Foot that started out: “The proliferation of modern programming languages (all of which seem to have stolen countless features from one another) sometimes makes it difficult to remember what language you're currently using. This guide is offered as a public service to help programmers who find themselves in such dilemmas.”
The list of languages starts with C and simply states:
This verdict might seem harsh, but holds more than just a grain of truth. However, even if alternative programming languages might be cursed with far less problems in terms of, for example, type safety and undefined behavior, they often lack features for programming close to the metal. If we stick with C we need to strike a balance—navigating between its obvious and not so obvious pitfalls while making the best use of its features. We can look at C for development of safety-critical functionality from two different perspectives:
If the products you are working on have a place in for example automotive, industrial control, medical devices, or railway, there’s a fair chance that they are subject to formal functional safety requirements. Such requirements can boil down to a very specific requirement on the tolerated failure rate of the product, or the allowed failure rate of some specific functionality in the product. It can also be a general requirement of the product being developed in accordance with a specific functional safety standard, like IEC61508 (electric and electronic programmable devices), ISO26262 (automotive), or EN 50126x (railway). For at least 10 years, a clear trend has been that implementation of safety functionality is moving away from pure mechanics or PLC controlled automation and into the microcontroller world, thus the requirements are spilling into the software domain.
Since the software requirements in the various standards are similar in intent, we will use IEC61508 as an example. This standard provides the foundation for many sector specific standards; requirements that are valid for IEC61508 will to a large extent be valid also for ISO26262 for example.
These standards will heavily influence the way you work and document your work: all the way from requirements gathering, to how you plan for deployment and decommissioning of the product at customer sites. It is not you and your project stakeholders alone that decide if you have succeeded in achieving the objectives in a chosen standard–you must also convince a third party assessor from an accredited body, or someone in your organization acting the part.
Most of these standards use variations of the safety integrity level concept. So depending on the classification of your product, there will be some variation in how you apply the appropriate standard.
Let’s get to the interesting question: What does all this have to do with my choice of programming language? Quite a lot, it turns out. The table below gives advice on how to select a suitable programming language depending on the safety integrity level of your application or safety function.
HR is short for Highly recommended, which in standards lingo it is actually a very strong indication that you should follow the recommendation or have a very good reason not to, i.e. a justification that you can back up 100 percent.
Table 1: Table from IEC61508 part 3 appendix A
As you can see in the table it is highly recommended to use a suitable programming language, which does not really make a lot of sense as far as recommendations go, does it? But the C appendix referenced in the table gives the following definition of a suitable programming language:
The language should be fully and unambiguously defined. The language should be user- or problem-orientated rather than processor/platform machine-orientated. Widely used languages or their subsets are preferred to special purpose languages. The language should encourage the use of small and manageable software modules; restriction of access to data in specific software modules; definition of variable sub-ranges; and any other type of error-limiting constructs.
Let’s look at the various parts of the above definition and how C stacks up to them:
C does not really live up to the expectations in the standard. What we can do about it?
Actually, the answer is quite simple, at least as long as we are only reading the standard. If we read on, we will find a table with judgments on specific languages and this is what it says for C:
Table 2: Table from IEC61508, part 7, appendix C
Even though C is not a recommended language, C with a suitable subset is even a highly recommended language if used together with a coding standard and static analysis tools. But what does subset and coding standard mean in this context?
The aim of a language subset in this context is to reduce the probability of programming errors and to increase the likelihood of finding such errors that has crept into the code base anyway. For C, this means eliminating the use of as many as possible of the undefined or implementation defined behaviors. There are a number of such language subsets available but the most widely known is probably MISRA-C. The MISRA-C rule set started out as an initiative by the Motor Industry Software Reliability Association in the UK and was aimed solely at automotive software. Over the years, the MISRA-C rules has spread over the world and into other industry segments, and the rule set is now the most widely used C subset in the embedded industry.
IEC61508 has quite a lot to say on the coding standard issue as well. The following are some examples of topics that should be considered in addition to the MISRA-C rules:
In essence, a coding standard should give advice on how to deal with issues that affect code quality and integrity but are not explicitly addressed by the language or the subset.
We will now touch some topics arising from the previous section and how to approach them in a project.
If you plan to use MISRA-C, the approach can be slightly different depending on if you start out with a clean slate or if you are reusing legacy code. For newly developed code, consider the following advice:
If you are applying a set of MISRA-C rules to legacy code it can be beneficial to:
We will now change focus to one of the more widely misunderstood areas of the C language: the volatile keyword. No matter who you ask, misuse of this keyword will go very high on the list of things that make embedded systems crash and burn.
The main reason to declare an object as volatile is to inform the compiler that the value of the object can change in ways unknown to the compiler and thus all accesses to the object must be preserved. There are three typical scenarios creating the need for volatile objects:
So, what kind of guarantees do you get from the compiler if you apply the volatile keyword to an object declaration? Essentially the following: All read and write accesses are preserved. That’s it!
Depending on the target architecture you might also get all accesses complete, performed in the order given in the abstract machine and, if applicable, atomic.
Figure 1: Compilation of volatile for an ARM/THUMB target
Is the code in figure 1 thread-safe and interrupt-safe, given that the volatile object can be accessed from different execution contexts? Both the load from memory and the store to memory of the value of vol are atomic since this is for a 32-bit load/store architecture. But the source statement is not atomic! We can still be hit by a context switch or interrupt somewhere in between the three instructions making up the vol++ statement.
Is your stack small and big enough? This is a perpetual conundrum for developers. If the stack is unnecessarily big, it might mean a part with more on-board RAM has to be used which drives costs. If the stack is too small, we might go down in flames, which for a product under safety-critical requirements is not good at all, to say the least. Here is a checklist of possible actions to consider when determining the stack size:
To sum up in a few important pointers, I would like to leave you with the following: