Embedded Code Safety Checks: Choosing Between if and assert

1. Introduction

In embedded system development, safety and reliability are crucial. When faced with input validation, boundary condition checks, and other scenarios, developers often need to choose between <span>if</span> condition checks and <span>assert</span> assertions. This article will analyze the applicable scenarios and best practices for both from a technical perspective.

2. Basic Differences Between if and assert

Feature if Condition Check assert Assertion
Compilation Impact Always present in the code Usually disabled in release versions
Runtime Cost Has runtime check overhead No overhead in release versions
Error Handling Can recover or gracefully degrade Typically terminates the program directly
Applicable Scenarios Expected error conditions Internal errors that should never occur

3. Special Considerations in Embedded Environments

In embedded systems, we need to consider additional factors:

  • Resource Constraints: ROM/RAM size, CPU performance
  • Reliability Requirements: Many embedded systems require 7×24 hours of stable operation
  • Debugging Difficulty: Field issues are hard to reproduce and diagnose
  • Safety Criticality: Special requirements in fields like medical, automotive, and industrial

4. When to Use if Condition Checks

Applicable Scenarios:

  • Validating external inputs (sensor data, communication messages, etc.)
  • Situations where resource allocation may fail (memory, peripherals, etc.)
  • Recoverable error conditions
  • Boundary checks for user-configurable parameters

Example Code:

bool sensor_read(Sensor* s, float* out_value) {
    if (s == NULL || out_value == NULL) {
        return false; // Invalid pointer is a foreseeable error
    }
    
    if (!s-&gt;initialized) {
        return false; // Uninitialized is a recoverable state
    }
    
    // ...actual read operation
    return true;
}

5. When to Use assert Assertions

Applicable Scenarios:

  • Internal invariant checks
  • Precondition validation for parameters during development
  • Conditions that theoretically should never occur
  • Severe error detection during debugging

Example Code:

void circular_buffer_push(CircularBuffer* cb, uint8_t data) {
    // These conditions should always be true in correctly used code
    assert(cb != NULL);
    assert(cb-&gt;buffer != NULL);
    assert(!circular_buffer_is_full(cb));
    
    // ...actual push operation
}

6. Key Decision Factors

Consider the following questions to decide which method to use:

  1. Is it a foreseeable runtime condition?

  • Foreseeable → <span>if</span>
  • Unforeseeable → <span>assert</span>
  • Is a check needed in the release version?

    • Needed → <span>if</span>
    • Only needed for debugging → <span>assert</span>
  • Is the error recoverable?

    • Recoverable → <span>if</span>
    • Unrecoverable → <span>assert</span>
  • Is the performance impact acceptable?

    • Acceptable → <span>if</span>
    • Unacceptable → <span>assert</span> (no overhead in release versions)

    7. Best Practices in Embedded Systems

    1. Layered Defense: Critical modules can use both <span>if</span> and <span>assert</span>, with <span>assert</span> capturing errors during development, and <span>if</span> handling errors in release versions

    2. Custom Assertion Macros:

    #ifdef DEBUG
    #define EMBEDDED_ASSERT(expr) \
        do { \
            if (!(expr)) { \
                log_error("Assert failed: %s, file %s, line %d", \
                         #expr, __FILE__, __LINE__); \
                system_halt(); \
            } \
        } while (0)
    #else
    #define EMBEDDED_ASSERT(expr) ((void)0)
    #endif
    
    1. Error Handling Strategies:

    • Safety-critical systems: Reset after entering a safe state
    • High-availability systems: Graceful degradation and error logging
  • Static Analysis Supplement: Use static analysis tools to discover potential issues and reduce runtime checks

  • 8. Case Studies

    Case 1: Pointer Parameter Check

    // External interface - use if
    int public_api_set_value(Device* dev, int value) {
        if (dev == NULL) {
            return ERROR_INVALID_PARAM;
        }
        // ...
    }
    
    // Internal function - use assert
    static void internal_helper(Device* dev) {
        assert(dev != NULL); // Caller should ensure dev is valid
        // ...
    }
    

    Case 2: Hardware Status Check

    bool read_temperature(float* temp) {
        if (temp == NULL) return false;
        
        if (!sensor_ready()) {
            log_warning("Sensor not ready");
            return false; // Recoverable state
        }
        
        uint16_t raw = read_sensor_register(0x55);
        assert(raw != 0xFFFF); // Hardware should not return this value
        
        *temp = convert_raw_to_temp(raw);
        return true;
    }
    

    9. Conclusion and Recommendations

    1. In product code, use if checks for all external inputs and foreseeable error conditions
    2. During development, extensively use <span>assert</span> to capture programming errors
    3. For critical modules, consider using both for layered defense
    4. Develop appropriate error handling strategies based on product requirements and safety levels
    5. On performance-sensitive paths, weigh safety and performance needs

    Remember: In embedded systems, an unhandled error can lead to catastrophic consequences. Properly utilizing a combination of <span>if</span> and <span>assert</span> can build robust and efficient embedded software.

    Leave a Comment