Hello! Welcome back to your course on functional safety.
In our last lesson, we established the "why" and "what" of defensive programming, exploring its principles as defined by ISO 26262 Part 6. You learned that these techniques are a primary defense against systematic failures.
Today, we transition from theory to practice. This lesson is dedicated to the implementation of the safety mechanisms we discussed. Your learning outcome is to implement software safety mechanisms, including parameter validation, range checks, and plausibility checks in a driver API. We'll focus on how to write code that is not just functional, but demonstrably robust and safe, a skill that is paramount for the senior embedded roles you are targeting.
1. The Design by Contract Philosophy
Before we write code, let's adopt a powerful mental model for implementing these checks: Design by Contract (DbC). This philosophy, when applied to embedded C, provides a clear and rigorous way to think about software safety.
The core idea is to distinguish between errors (bugs) and exceptional conditions.
- An exceptional condition is a rare but legitimate event the system must handle (e.g., a communication partner not responding).
- An error (bug) is a programming mistake that should never happen in a correct system (e.g., passing a
NULLpointer to a function that doesn't expect one).
In many embedded systems, what might be an "exceptional condition" on a desktop PC is actually a bug. For instance, running out of memory. There's often no way for an embedded device to recover gracefully, so it should be treated as a bug.
The following article explains this distinction and introduces the DbC philosophy, which is a perfect framework for our safety mechanisms.
Design by Contract for Embedded C
This article, "Design by Contract for Embedded C" from the Barr Group, is a foundational text on this topic. It argues that many conditions in embedded systems should be treated as bugs, not exceptions, and introduces assertions as the primary tool for enforcing these "contracts."
Please read the sections "Errors versus exceptional conditions," "Design by contract (DbC)," and "The DbC philosophy." Pay close attention to: The argument for why a failed malloc() in an embedded system is often a bug. How DbC uses assertions to detect bugs early, rather than trying to "handle" them. The analogy of assertions to electrical fuses: they provide controlled failure to help localize a problem.
The key takeaway is that your driver's API forms a contract with its callers. The caller must provide valid parameters (preconditions), and in return, the driver guarantees correct operation. If the caller violates the contract, it's a bug. Our goal is to detect that violation immediately.
2. Parameter Validation and Range Checks
This is your first line of defense, directly enforcing the "preconditions" of your API's contract. You must validate every input that comes from an external caller before you use it to interact with hardware.
These checks typically fall into two categories:
- Validity Checks: Is a pointer
NULL? Is an enumerator a valid value? - Range Checks: Is a numerical value within its defined minimum and maximum bounds?
Implementation in a Driver API
While the DbC article advocates for assertions (ASSERT), which are excellent during development, production code in a safety-critical context like AUTOSAR usually requires a more controlled error-handling mechanism. You've used Det_ReportError before; this is the production-grade equivalent of a failed assertion. The logic is the same: detect a contract violation, report it, and prevent unsafe operation.
Let's write a function for a hypothetical ADC driver.
Non-Defensive Version:
/* Reads the raw value from the specified ADC channel. */
void Adc_Read(uint8_t channel, uint16_t* result) {
// Selects the channel in the hardware register
ADC_REG->CH_SEL = channel;
// Starts a conversion
ADC_REG->CTRL |= ADC_START_CONVERSION;
// Waits for the conversion to be done
while ((ADC_REG->STATUS & ADC_EOC_FLAG) == 0);
// Crashes here if result is NULL!
*result = ADC_REG->DATA;
}
This code has several critical flaws. If channel is out of bounds, it might select a non-existent channel or corrupt an adjacent register. If result is NULL, the system will crash with a memory fault.
Defensive Version (AUTOSAR Style):
#include "Det.h" // Assume AUTOSAR Development Error Tracer
#define ADC_MAX_CHANNELS 8
/* Reads the raw value from the specified ADC channel. */
Std_ReturnType Adc_Read(uint8_t channel, uint16_t* result) {
// --- Contract Enforcement ---
// 1. Parameter Validation: Pointer Check
if (result == NULL) {
Det_ReportError(ADC_MODULE_ID, ADC_INSTANCE_ID, ADC_API_ID_READ, ADC_E_PARAM_POINTER);
return E_NOT_OK;
}
// 2. Parameter Validation: Range Check
if (channel >= ADC_MAX_CHANNELS) {
Det_ReportError(ADC_MODULE_ID, ADC_INSTANCE_ID, ADC_API_ID_READ, ADC_E_PARAM_CHANNEL);
return E_NOT_OK;
}
// --- Business Logic (only if contract is met) ---
ADC_REG->CH_SEL = channel;
ADC_REG->CTRL |= ADC_START_CONVERSION;
while ((ADC_REG->STATUS & ADC_EOC_FLAG) == 0);
*result = ADC_REG->DATA;
return E_OK;
}
This version is robust. It validates all inputs before touching any hardware, provides clear error reporting, and returns a status to the caller, fulfilling the principles of ISO 26262.
The "Best Practice Guideline" you've seen before reinforces these techniques. It highlights the need to check input values and analyze boundary values during testing to ensure these checks are effective.
Software for Safety-Related Automotive Systems
This guideline document provides industry context for these practices. The following sections reinforce the importance of defensive checks and the testing required to validate them.
Please review the following methods in the document: Table 6, Method 1d, 'Use of defensive implementation techniques': Note the examples like "Plausibility checks of input values of functions." Table 7, Method 1k, 'Interface test': This describes testing your API by varying inputs to detect "ineffective or missing range checks." Table 8, Method 1c, 'Analysis of boundary values': This explains how to test range checks effectively by focusing on the min, max, min-1, and max+1 values.
Test your understanding!
You are writing a Pwm_SetDutyCycle(uint8_t channelId, uint8_t duty) function for a PWM driver. The duty cycle is a percentage from 0 to 100, inclusive. Write the parameter validation and range checks for this function's entry point in the defensive AUTOSAR style. Assume PWM_MAX_CHANNELS is defined.
Show answer
Std_ReturnType Pwm_SetDutyCycle(uint8_t channelId, uint8_t duty) {
// 1. Range check on channelId
if (channelId >= PWM_MAX_CHANNELS) {
Det_ReportError(PWM_MODULE_ID, PWM_INSTANCE_ID, PWM_API_ID_SETDUTY, PWM_E_PARAM_CHANNEL);
return E_NOT_OK;
}
// 2. Range check on duty cycle
if (duty > 100) { // Since duty is uint8_t, it cannot be < 0.
Det_ReportError(PWM_MODULE_ID, PWM_INSTANCE_ID, PWM_API_ID_SETDUTY, PWM_E_PARAM_VALUE);
return E_NOT_OK;
}
// ... proceed with setting the PWM duty cycle ...
return E_OK;
}
3. Plausibility Checks: Context-Aware Validation
Plausibility checks are a deeper, more intelligent form of validation. While a range check verifies if a value is valid, a plausibility check verifies if it makes sense in the current system state or context.
For example:
duty_cycle = 150-> Fails a range check.duty_cycle = 50, but the PWM module hasn't been initialized yet -> Fails a plausibility check.
The "Best Practice Guideline" gives a classic automotive example: receiving a vehicle speed of 0 km/h just 20ms after receiving 100 km/h is not plausible.
This flowchart shows a real-world, multi-step plausibility check from an engine control unit.

Your ADAS background provides many more examples. An object detection system might use plausibility checks to filter out noise.

Implementation in a Driver API
In a driver, plausibility checks often relate to the driver's internal state machine. You must ensure that API calls are made in the correct order.
Let's revisit the Can_SetBaudrate example.
Defensive Version with Plausibility Check:
// Assume an internal state variable for the driver
static Can_DriverStateType G_can_driver_state = CAN_UNINIT;
Std_ReturnType Can_SetBaudrate(uint32_t baudrate) {
// 1. Plausibility Check: Is the driver initialized?
if (G_can_driver_state == CAN_UNINIT) {
Det_ReportError(..., CAN_E_UNINIT);
return E_NOT_OK;
}
// 2. Plausibility Check: Is the controller in a state that allows this change?
// Baudrate can only be set when the controller is stopped.
if (CAN_HW_REGS->STATE != CAN_STATE_STOPPED) {
Det_ReportError(..., CAN_E_WRONG_STATE);
return E_NOT_OK;
}
// 3. Range Check: Is the baudrate value itself supported?
if (!IsBaudrateSupported(baudrate)) {
Det_ReportError(..., CAN_E_PARAM_BAUDRATE);
return E_NOT_OK;
}
// --- Business Logic ---
// ... code to configure the hardware registers ...
return E_OK;
}
Here, we check both the software state (CAN_UNINIT) and the hardware state (CAN_STATE_STOPPED) before proceeding. This is a much safer implementation than one that only checks the baudrate value itself.
4. Advanced Perspective: Preemptive Programming
Finally, let's refine our mindset further. The "Design by Contract" article distinguishes between two meanings of "defensive programming." One is making code robust by silently handling bad inputs. The other, preferred by DbC, is to be intolerant of bad inputs and to fail fast and loud. This proactive approach can be called "preemptive programming."
Design by Contract for Embedded C
This final reading from the DbC article contrasts two styles of defensive programming and makes a strong case for shipping products with assertions enabled.
Please read the sections "Defensive or preemptive programming?", the paragraph on using hardware peripherals for assertions, and "Shipping with assertions." Notice how the 'defensive' Port class silently hides bugs, while the DbC version makes misuse difficult. Consider the idea of a 'real-time assertion', where you use a timer to assert that a task meets its deadline. Reflect on the argument for leaving assertions enabled in production code, especially the analogy to fuses.
For an interview, understanding this nuance is a sign of deep expertise. While production AUTOSAR code uses Det and error returns, the underlying principle is to detect and report violations, not hide them. A system that hangs because of a silent failure is often far more dangerous than one that detects an integrity violation and performs a controlled reset.
Conclusion
In this lesson, you have moved from the theory of defensive programming to the practical implementation of key software safety mechanisms. You have learned how to write robust driver APIs that can withstand invalid usage and contribute to the overall safety of the system.
Key Takeaways:
- Parameter validation and range checks are the first line of defense at an API boundary, enforcing the function's contract.
- Plausibility checks provide a deeper, context-aware layer of safety by validating that an operation makes sense in the current system state.
- The Design by Contract (DbC) philosophy provides a rigorous framework, treating invalid API usage as a bug to be detected immediately, not an exception to be handled.
- Production-grade safety code (like in AUTOSAR) implements these checks using
ifstatements that report errors via modules like DET and returnE_NOT_OK, which is a formalized version of a failed assertion.
Next Lesson Preview
We have now secured our driver's API interface. But what about the integrity of the system's core components, like memory and execution flow? In our next lesson, we will address this by learning how to implement a startup RAM test (e.g., March C-) and a periodic watchdog refresh mechanism. These are fundamental safety mechanisms for detecting hardware faults and ensuring the program hasn't frozen.