Unit Testing Frameworks : Unit testing is a software development practice where individual units or components of a program are tested to ensure they behave as expected. In embedded systems, especially when working with complex software, it becomes critical to verify the correctness of small parts of the code independently. Unit testing frameworks help automate this process, making it easier and more reliable.
One of the most popular unit testing frameworks is Google Test, which is widely used for testing C++ code. In this article, we’ll explore Google Test, how to set it up, and how to write basic unit tests.
Introduction to Unit Testing Frameworks
What is Google Test?
Google Test (also known as gtest) is an open-source C++ testing framework developed by Google. It helps developers write and run tests to verify their code. It is designed to be simple, easy to use, and integrate into different types of C++ projects, including embedded systems, application software, and libraries.
Some features of Google Test include:
Assertions: Google Test provides various types of assertions to check if your code behaves as expected (e.g., EXPECT_EQ(), ASSERT_EQ()).
Test Fixtures: You can set up common test data and configuration before running tests.
Parameterized Tests: You can run tests with different inputs, improving test coverage.
Setting Up Google Test
Before you can use Google Test in your project, you need to set it up. Here’s a simple guide on how to integrate it into your C++ project.
Step 1: Install Google Test
To install Google Test, follow these steps:
Clone the Google Test repository from GitHub: git clone https://github.com/google/googletest.git cd googletest
Build Google Test: If you’re using CMake (which is common in embedded development), you can build it as follows: mkdir build cd build cmake .. make sudo make install
Step 2: Include Google Test in Your Project
In your C++ project, link the Google Test library and include the necessary headers.
Include the header file in your test source code: #include <gtest/gtest.h>
Link the Google Test library during the compilation. If you’re using CMake, your CMakeLists.txt file would look something like this: find_package(GTest REQUIRED) include_directories(${GTEST_INCLUDE_DIRS}) add_executable(my_tests test.cpp) target_link_libraries(my_tests ${GTEST_LIBRARIES} pthread)
Writing Your First Unit Test
Now that you have Google Test set up, it’s time to write your first unit test.
Example: Testing a Simple Function
Let’s assume we have a simple function that adds two numbers:
int Add(int a, int b) {
return a + b;
}
write test cases
#include <gtest/gtest.h>
// Function to be tested
int Add(int a, int b) {
return a + b;
}
// Test case
TEST(AdditionTest, HandlesPositiveInput) {
EXPECT_EQ(Add(2, 3), 5); // Assert that 2 + 3 equals 5
}
TEST(AdditionTest, HandlesNegativeInput) {
EXPECT_EQ(Add(-2, -3), -5); // Assert that -2 + -3 equals -5
}
TEST(AdditionTest, HandlesZeroInput) {
EXPECT_EQ(Add(0, 0), 0); // Assert that 0 + 0 equals 0
}
// Main function to run the tests
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Compile and run the tests: Use the following command to compile your test code: g++ -std=c++11 -isystem /path/to/gtest/include -pthread test.cpp /path/to/gtest/libgtest.a -o test_program ./test_program If everything is set up correctly, Google Test will run your tests and display the results.
Assertions in Google Test
Google Test provides various types of assertions to verify the results of your code. Some commonly used assertions include:
EXPECT_EQ(val1, val2): Checks if two values are equal.
EXPECT_NE(val1, val2): Checks if two values are not equal.
ASSERT_TRUE(condition): Checks if the condition is true.
ASSERT_FALSE(condition): Checks if the condition is false.
EXPECT_LT(val1, val2): Checks if val1 is less than val2.
EXPECT_GT(val1, val2): Checks if val1 is greater than val2.
Here’s an example of using assertions:
TEST(SubtractionTest, HandlesPositiveInput) {
EXPECT_EQ(Add(5, 3), 8); // Passes
ASSERT_FALSE(Add(5, 3) == 7); // Passes, checks that 5 + 3 is not equal to 7
}
Running Tests and Viewing Results
After compiling and running the tests, Google Test will display the results in the terminal. It will show whether each test passed or failed. If a test fails, it provides details on the failure, such as which assertion failed and the values involved.
For example, if the Add() function is incorrect, Google Test will display:
[ RUN ] AdditionTest.HandlesPositiveInput
test.cpp:9: Failure
Expected equality of these values:
Add(2, 3)
Which is: 4
5
[ FAILED ] AdditionTest.HandlesPositiveInput (0 ms)
Test Fixtures
Test fixtures allow you to set up common test data or objects that are shared across multiple test cases. This is useful for avoiding code duplication.
Example:
class AdditionTest : public ::testing::Test {
protected:
int a = 2;
int b = 3;
};
TEST_F(AdditionTest, HandlesPositiveInput) {
EXPECT_EQ(Add(a, b), 5);
}
Conclusion
Google Test is a powerful and flexible unit testing framework that can help you ensure the correctness of your code. By writing automated tests, you can catch bugs early and ensure your software behaves as expected. For embedded systems and other C++ projects, Google Test makes testing easy and efficient.
In this article, we introduced the basics of setting up and using Google Test, writing test cases, using assertions, and running tests. By integrating unit tests into your development workflow, you’ll improve the reliability and maintainability of your software.
Basic Questions
What is Google Test (gtest)?
Explain what Google Test is and its key features.
What are the advantages of using Google Test in C++ projects?
Discuss the benefits of using Google Test over manual testing or other testing frameworks.
How do you install and set up Google Test in a C++ project?
Describe the process of installing and integrating Google Test with a project using either CMake or Makefiles.
What is the difference between ASSERT_* and EXPECT_* in Google Test?
Explain when to use ASSERT_* and EXPECT_* assertions.
What is a test fixture in Google Test?
Describe what a test fixture is and provide an example use case.
Can you explain the structure of a basic Google Test test case?
How do you write a simple test case using TEST or TEST_F?
What is the role of the main function in Google Test?
What does the main function do in a Google Test framework?
What is a parameterized test in Google Test, and how do you implement one?
Explain what parameterized tests are and how to use the INSTANTIATE_TEST_SUITE_P macro.
What is the difference between RUN_ALL_TESTS() and RUN_ALL_TESTS() in Google Test?
Explain the differences in test execution control in Google Test.
How do you handle exceptions in Google Test?
Can Google Test catch and test exceptions thrown by your code?
Intermediate Questions
Explain the concept of test cases and test suites in Google Test.
What is a test case and how does it fit into a test suite?
How do you organize multiple tests in Google Test for better readability and maintenance?
Discuss strategies for organizing tests, such as naming conventions or categorizing tests into test suites.
How would you implement mock objects in Google Test?
Explain how Google Mock can be used alongside Google Test to mock dependencies for unit testing.
How do you perform performance testing using Google Test?
Is it possible to use Google Test for performance benchmarking, and if so, how?
What are the best practices for writing effective unit tests using Google Test?
Discuss the best practices such as keeping tests small, independent, and readable.
How do you integrate Google Test with continuous integration (CI) tools like Jenkins?
Describe the process of integrating Google Test results into a CI/CD pipeline.
Explain how to run Google Test on embedded systems (such as ARM or RTOS) and handle cross-compilation.
Discuss challenges and solutions for running Google Test in embedded environments.
How would you debug a failing test in Google Test?
What strategies do you use to diagnose and fix failing tests in Google Test?
What are the types of assertions available in Google Test, and when would you use each one?
Provide a list of assertions such as EXPECT_EQ, EXPECT_NE, ASSERT_TRUE, and their appropriate use cases.
How can you use Google Test to test multithreaded code?
Explain how to write tests for code involving threads and concurrency.
Advanced Questions
What are Google Test’s test discovery and test filtering mechanisms?
How can you control which tests to run using test discovery and filtering options?
What are the advantages of using Google Test over other unit testing frameworks like Catch2 or Boost.Test?
Compare and contrast Google Test with other popular C++ testing frameworks.
Can you explain Google Test’s DeathTest feature?
What is a “Death Test,” and when would you use it in Google Test?
How do you test code that interacts with hardware or I/O in Google Test?
Discuss techniques for testing hardware-dependent code in unit tests, such as mocking hardware APIs.
How would you create custom assertions in Google Test?
Describe how to create custom assertions to extend Google Test’s built-in functionality.
How do you handle memory management issues in tests?
Discuss strategies for managing memory leaks and other memory issues when running tests in Google Test.
How do you use Google Test with a version-controlled project (e.g., Git)?
Discuss strategies for using Google Test in teams, version control, and managing test cases.
Explain the role of Google Test in TDD (Test-Driven Development).
How does Google Test support the Test-Driven Development approach in embedded software?
How would you mock external dependencies (e.g., databases, network calls) in Google Test?
Provide examples of mocking external systems to isolate the unit being tested.
How do you optimize the runtime of Google Test when testing large codebases?
Discuss techniques for reducing the time it takes to run tests, such as test grouping or test prioritization.
You can also Visit other tutorials of Embedded Prep
Static code analysis is a method of examining software code without executing it. It is a technique used to find potential errors, bugs, security vulnerabilities, and inefficiencies in code early in the development process. This is especially valuable in embedded systems and software development, where bugs can be costly, hard to trace, and difficult to fix once deployed.
What is Static Code Analysis?
Static code analysis involves scanning the source code using specialized tools, often referred to as Static Analysis Tools, to detect issues such as:
Syntax Errors: Problems with the structure of the code.
Logic Errors: Flaws that might not be caught by compilers but could lead to unexpected behavior.
Security Vulnerabilities: Issues that may expose the software to security risks.
Code Smells: Signs of inefficient or hard-to-maintain code.
Memory Leaks and Unused Variables: Common problems in embedded and C/C++ programming.
These tools do not require the program to be run and can be used at any stage of the development process. The goal is to identify issues early, which can save time, effort, and resources compared to fixing bugs after deployment.
How Does Static Code Analysis Work?
Parsing the Code: The tool reads and interprets the code’s structure, including functions, loops, variables, and conditionals.
Rule Matching: The tool compares the code against a set of predefined rules (best practices, coding standards, and error patterns).
Report Generation: Once the analysis is complete, the tool generates a report highlighting any issues, warnings, or suggestions.
Fixing Issues: Developers review the report and address any issues, improving the quality and reliability of the code.
Why Use Static Code Analysis?
Improve Code Quality: Helps identify bugs, errors, and inefficiencies early in development.
Security: Detects vulnerabilities that could be exploited in real-world scenarios.
Better Maintainability: Promotes cleaner, more readable, and easier-to-maintain code.
Cost-Effective: It’s more cost-effective to catch issues during the development phase than after the software has been deployed.
Introduction to Cppcheck
Cppcheck is an open-source static code analysis tool specifically designed for C and C++ code. It is widely used in embedded systems and general software development to identify bugs, security vulnerabilities, and violations of coding standards.
Cppcheck works by analyzing source code to detect potential issues such as:
Memory leaks
Null pointer dereferencing
Unused functions and variables
Undefined behavior
Cppcheck is different from other tools like compilers because it does not rely on running the program but rather looks at the code statically. It is flexible, easy to use, and customizable.
How to Use Cppcheck
Let’s walk through using Cppcheck with a simple C++ example:
Here’s an example C++ code that we will analyze using Cppcheck. This code has a few intentional issues for demonstration purposes:
#include <iostream>
void demoFunction() {
int* ptr = nullptr;
std::cout << "Hello World!" << std::endl;
*ptr = 42; // Dereferencing a null pointer (dangerous!)
}
int main() {
int x = 10;
int y = 20;
if (x > y) {
std::cout << "x is greater than y" << std::endl;
} else {
std::cout << "y is greater than x" << std::endl;
}
return 0;
}
In the above code:
Line 6: Dereferencing a null pointer.
Line 14-16: The if-else block compares two variables but is not logically necessary.
Step 3: Running Cppcheck
To analyze this code with Cppcheck, navigate to the directory where the file is saved and run the following command:
cppcheck demo.cpp
Step 4: Understanding the Output
Cppcheck will output warnings or errors if it detects any issues in the code. For the provided example, you might see output like this:
demo.cpp: In function 'void demoFunction()':
demo.cpp:6:5: warning: Dereferencing null pointer [nullPointerDereference]
demo.cpp:14:5: warning: The 'if' condition is always false [logicalError]
nullPointerDereference: Warns about dereferencing a null pointer, which could lead to a crash.
logicalError: Indicates that the comparison between x and y will always return false because of the if (x > y) condition.
Step 5: Fixing the Issues
Null Pointer Dereference: Instead of dereferencing the null pointer, you can add a check before dereferencing: if (ptr != nullptr) { *ptr = 42; }
Unnecessary Comparison: You can simplify the logic for comparing x and y: if (x != y) { std::cout << (x > y ? "x is greater than y" : "y is greater than x") << std::endl; }
Step 6: Re-run Cppcheck
Once the issues have been fixed, you can re-run Cppcheck to ensure that the code is now free of issues:
cppcheck demo.cpp
If there are no issues, Cppcheck will provide no output (unless you specify an option to show info-level warnings).
Conclusion
Static code analysis is an essential practice for maintaining high-quality software, especially in embedded and C/C++ development. Tools like Cppcheck help developers detect issues early, improve code quality, and reduce long-term maintenance costs.
Using Cppcheck is simple, and with regular use, it can save you from debugging complex issues later in the development process. By integrating static analysis into your workflow, you ensure that your code is more reliable, secure, and easier to maintain.
Here’s a list of interview questions on Static Code Analysis Tools with a focus on Cppcheck:
Basic Questions:
What is static code analysis, and how does it differ from dynamic code analysis?
What are the benefits of using static code analysis tools in the software development lifecycle?
Can you explain the basic functioning of Cppcheck?
How does Cppcheck identify potential bugs or issues in the code?
What types of issues can Cppcheck detect in C/C++ code?
How do static analysis tools help in improving code quality?
What are the advantages of using Cppcheck over other static analysis tools?
How would you integrate Cppcheck into a CI/CD pipeline?
Can you give an example of a scenario where static code analysis is crucial in embedded systems development?
Intermediate Questions:
What are the most common types of warnings or errors that Cppcheck detects?
Explain the difference between false positives and false negatives in static code analysis tools.
What are the different types of checks that Cppcheck performs (e.g., memory leaks, null pointer dereferencing)?
How would you configure Cppcheck to check for specific coding standards or rules in your project?
What are some limitations of Cppcheck in comparison to other static analysis tools like Clang Static Analyzer or SonarQube?
Explain the concept of “code smells” and how Cppcheck helps in identifying them.
Can you modify the rule set or configure custom rules in Cppcheck? How would you do that?
What are some common coding mistakes or bad practices that Cppcheck helps to identify?
How do you interpret and resolve the warnings or errors provided by Cppcheck?
Advanced Questions:
How would you optimize the performance of Cppcheck when analyzing large codebases?
Can Cppcheck be used for code quality checks in both embedded systems and applications? Explain why or why not.
What are the various options available in Cppcheck to customize the output (e.g., format, severity levels, etc.)?
Explain how Cppcheck integrates with version control systems like Git to perform automated code analysis during code commits or pull requests.
Can you use Cppcheck with other programming languages apart from C/C++? If yes, how?
What are the main performance-related issues that Cppcheck detects in embedded systems code?
How would you address memory leak warnings in embedded systems detected by Cppcheck?
Have you ever encountered a situation where Cppcheck generated false positives? How did you handle it?
How does Cppcheck deal with external libraries or third-party dependencies in the codebase?
What are the trade-offs between using static analysis and other testing methods (like unit testing or dynamic analysis) for embedded systems?
Describe a situation where static code analysis led to the discovery of a critical bug that would have been hard to detect through manual testing.
Scenario-Based Questions:
You are working on a safety-critical embedded system. How would you ensure the reliability of the code using static code analysis tools like Cppcheck?
You have a legacy codebase with several issues. How would you approach using Cppcheck to clean up the code and improve its maintainability?
While using Cppcheck, you find multiple warnings about unused variables in your code. How would you resolve this issue?
During static code analysis with Cppcheck, you notice a warning related to an uninitialized pointer. How would you address this problem?
How would you configure Cppcheck to generate reports for a large-scale project while focusing on high-priority issues?
Imagine you are working with a team to maintain a large embedded codebase. How would you ensure consistent static analysis across the team using Cppcheck?
Tool-Specific Questions:
Explain how Cppcheck handles complex C++ features like templates, inheritance, and polymorphism.
How do you interpret Cppcheck’s output when it reports a “nullPointerDereference” warning?
What are the possible reasons for Cppcheck to miss certain errors or issues?
What are the command-line options you can use with Cppcheck to improve the efficiency of the analysis?
Can you integrate Cppcheck into a development IDE like Visual Studio or Eclipse? If so, how?
What does the Cppcheck warning unusedFunction mean, and how can you handle it?
Can you use Cppcheck to perform static analysis on code that includes both C and C++ files?
What is the purpose of the --enable=all option in Cppcheck, and when would you use it?
How would you handle performance issues when running Cppcheck on large codebases with thousands of lines of code?
You can also Visit other tutorials of Embedded Prep
System testing is an essential phase in software development where the entire system is tested as a whole to ensure it functions as intended. It focuses on validating both the functional and non-functional requirements of a system. This tutorial will guide you through the basics of system testing, its objectives, types, and how to perform it effectively.
What is System Testing?
System testing is the process of testing the complete and integrated software system to verify that it meets the specified requirements. It checks if the system behaves as expected in all scenarios, considering both functional (features, functionality) and non-functional (performance, security) requirements.
Key Objectives of System Testing
Verify functionality: Ensure the system performs all the intended functions correctly.
Detect defects: Identify any bugs or issues that may cause the system to behave unexpectedly.
Ensure reliability: Check if the system operates consistently under various conditions.
Assess performance: Evaluate if the system performs well under different loads and stress conditions.
Validate compatibility: Ensure the system works on different hardware, operating systems, and browsers (if applicable).
Types of System Testing
System testing can be broken down into several types, each focusing on a specific aspect of the system:
Functional Testing: Validates that the system behaves according to the functional specifications.
Example: Testing whether the user login feature works as expected.
Performance Testing: Assesses how the system performs under different workloads.
Example: Testing how the system responds when multiple users log in at the same time.
Security Testing: Ensures that the system is secure from threats and unauthorized access.
Example: Testing for SQL injection vulnerabilities in a web application.
Usability Testing: Validates the ease of use and user-friendliness of the system.
Example: Testing if a user can easily navigate through a website without confusion.
Compatibility Testing: Checks if the system works correctly across different environments (e.g., OS, browsers, hardware).
Example: Testing if a web application works on Chrome, Firefox, and Edge.
Stress Testing: Tests the system under extreme conditions, such as high traffic or heavy resource usage.
Example: Testing how a website behaves when receiving thousands of requests in a short period.
Regression Testing: Ensures that new changes or bug fixes have not negatively impacted the existing system.
Example: After fixing a bug in the payment feature, test the entire system to ensure nothing else is broken.
Steps in System Testing
Understand the System Requirements
Review the functional and non-functional requirements of the system. These will be the basis for your test cases.
Develop Test Plan
A test plan outlines the scope, objectives, and approach for system testing. It also includes the resources, schedule, and risk assessment.
Design Test Cases
Based on the system requirements, create test cases that cover all aspects of the system.
Test cases should include:
Test description
Expected results
Steps to execute the test
Input data
Expected outputs
Set Up the Test Environment
Prepare the hardware, software, and network environment for testing. This may involve setting up servers, databases, and testing tools.
Execute Test Cases
Run the test cases and observe the system’s behavior. Record the actual results and compare them with the expected results.
Defect Reporting
If any issues are found, report them to the development team for fixing. Defects should be logged with detailed information, including the steps to reproduce the issue.
Retest
Once defects are fixed, retest the system to ensure the issues have been resolved and that no new issues have been introduced.
Test Completion
Once all the test cases have been executed, and the system is validated, generate a test summary report that includes the results of the testing and any unresolved issues.
Example of a Simple System Test Case
Let’s say you are testing a login functionality on a website. Here’s an example test case:
Test Case ID: TC001
Test Description: Verify that the user can log in with valid credentials.
Test Steps:
Open the login page.
Enter a valid username and password.
Click the “Login” button.
Expected Result: The user should be redirected to the dashboard.
Actual Result: The user is redirected to the dashboard (Assumed Passed).
Best Practices for System Testing
Start Early: Begin system testing early in the software development life cycle (SDLC) to detect defects as soon as possible.
Test in Different Environments: Make sure to test the system in various environments to ensure compatibility.
Automate Repetitive Tests: Use test automation tools to run repetitive tests, especially for regression testing.
Maintain Clear Documentation: Keep detailed logs of test cases, test results, defects, and fixes.
Involve Different Stakeholders: Include various team members such as developers, QA testers, and business analysts to get different perspectives on the system.
Prioritize Critical Areas: Focus on testing critical areas of the system, such as security and performance, before others.
Tools for System Testing
There are many tools available to assist in system testing, depending on the type of testing you need to perform. Here are some common tools:
Selenium: For automating web application testing.
JMeter: For performance and load testing.
Postman: For testing APIs.
OWASP ZAP: For security testing.
TestComplete: For automated functional testing.
LoadRunner: For performance and load testing.
Conclusion
System testing is an important step in ensuring the software functions correctly and meets all requirements. By following the steps and best practices outlined in this tutorial, you can effectively carry out system testing, identify defects, and ensure that the system is ready for deployment.
You can also Visit other tutorials of Embedded Prep
MISRA C MISRA C++: Are you new to embedded systems programming and want to write safe, secure, and reliable C/C++ code? This beginner-friendly tutorial is your ultimate guide to understanding and applying MISRA-C and MISRA-C++ guidelines in real-world embedded projects.
In this MISRA C MISRA C++ Guidelines 2025 edition, we’ll walk you through:
✅ What is MISRA and why it’s important for automotive, aerospace, medical, and other safety-critical industries ✅ Detailed explanation of key MISRA-C and MISRA-C++ rules, including examples and rationale ✅ How to write compliant code step-by-step, avoiding undefined behavior and bugs ✅ Common pitfalls and how to fix violations in your code ✅ Using static analysis tools (like Parasoft, PC-lint, etc.) to check for compliance ✅ Practical tips for integrating MISRA compliance into your development workflow
This tutorial is crafted to help beginners and professionals alike gain confidence in writing MISRA-compliant code—one concept at a time.
Let’s make your embedded software robust, maintainable, and industry-ready in 2025!
MISRA C MISRA C++ :
What is MISRA?
MISRA stands for Motor Industry Software Reliability Association. It is a set of software development guidelines for the C and C++ programming languages, especially designed to:
Improve code reliability, portability, and safety
Avoid undefined behavior and compiler-specific behavior
Ensure code can be used in critical systems, especially in automotive, aerospace, medical, and industrial software
Why Use MISRA-C MISRA-C++?
In embedded and safety-critical systems, even a small bug can lead to huge failures. MISRA helps you write safe, predictable, and standardized code that:
Avoids risky language features
Is easier to analyze and test
Can be certified for safety (like ISO 26262 in automotive)
Difference Between MISRA C MISRA C++
Feature
MISRA-C
MISRA-C++
Language Focus
C (especially C90, C99, C11)
C++ (mainly C++03, some C++11)
Use Case
Widely used in embedded C
Less common, but used in modern automotive software (e.g., ADAS)
Latest Version
MISRA C:2012 (with amendments)
MISRA C++:2008
Structure of MISRA C MISRA C++ Guidelines
Each rule is categorized by:
1. Category
Mandatory – Must always be followed
Required – Should be followed; deviations must be documented
Advisory – Good practice; optional
2. Rule Types
Directive – High-level rule (e.g., All code should be traceable to a requirement)
Functions should have internal linkage if not used outside the file
✅ static void helper()
Rule 11.4
Avoid casting between pointers to different object types
❌ int *x = (int*)ptr;
Rule 17.7
Do not use goto
❌ goto error;
Rule 21.1
Use of standard library functions should be limited
❌ system("rm -rf /")
Common MISRA-C++ Rules (Simplified Examples)
Rule
Description
Example
Rule 0-1-1
Avoid compiler-specific extensions
❌ __attribute__((packed))
Rule 5-0-15
Do not use reinterpret_cast
❌ reinterpret_cast<int*>(ptr)
Rule 6-4-1
Do not use multiple inheritance
❌ class A: public B, public C
Rule 7-1-1
Avoid using new and delete
Prefer static memory or smart pointers
How to Start Using MISRA in Your Project
Read the Guidelines
Official documents are paid but summaries are available online
Use a Static Code Analyzer Tools like:
Parasoft
PC-lint / FlexeLint
Coverity
Cppcheck (partial support)
Write Clean and Portable Code Follow:
No goto
No dangerous casts
No dynamic memory unless necessary
Always initialize variables
Use Compiler Warnings Set high warning levels and treat warnings as errors.
Example MISRA-Compliant C Code
#include <stdint.h>
static int32_t add_numbers(int32_t a, int32_t b) {
int32_t result = a + b; // Always initialized
return result;
}
int main(void) {
int32_t sum = add_numbers(5, 10);
return 0; // No dynamic memory, no undefined behavior
}
Example: Low-level firmware, device drivers, or bare-metal embedded systems.
You’re working with microcontrollers with limited memory and no operating system.
Safety and reliability are critical (automotive ECUs, medical devices, industrial sensors).
You want to avoid risky or undefined behaviors in C (e.g., pointer misuse, buffer overflows).
You need to follow standards like ISO 26262, DO-178C, IEC 61508, etc.
Common Domains: Automotive, medical devices, industrial automation, robotics, defense.
Use MISRA-C++ When:
Your project uses C++ instead of C.
You’re building complex embedded applications using object-oriented design (e.g., control systems, infotainment systems).
You want to leverage C++ features (e.g., classes, inheritance, templates) but still need safety and predictability.
Your team has C++ experience and your toolchain supports it.
You need to comply with C++-specific safety standards (e.g., AUTOSAR Adaptive Platform).
Common Domains: Automotive (infotainment, ADAS), drones, robotics, aerospace control systems.
Still Confused?
Use MISRA-C if you’re writing in pure C, especially for low-level or real-time systems.
Use MISRA-C++ if your embedded project uses modern C++, and you want to combine safety with high-level features.
Summary
Term
Meaning
MISRA
Coding rules for safety-critical software
Mandatory
Must follow
Required
Should follow (document if not)
Static Tool
Tool to check MISRA violations
Beginner-Level Questions
What is MISRA? Why is it important?
What are the goals of MISRA-C / MISRA-C++?
What is the difference between MISRA-C and MISRA-C++?
What do “Mandatory”, “Required”, and “Advisory” rules mean?
What is undefined behavior in C, and how does MISRA help avoid it?
Why does MISRA disallow the use of goto?
Why should dynamic memory (malloc, new) be avoided in embedded systems according to MISRA?
Why does MISRA suggest using static for functions not used outside the file?
Why is type casting between incompatible types discouraged in MISRA?
Can you name a few standard C functions that MISRA recommends avoiding?
Intermediate-Level Questions
Explain the difference between a directive and a rule in MISRA.
How does MISRA ensure portability across compilers and architectures?
What are some limitations or criticisms of MISRA rules?
How do you apply MISRA rules in a real project?
What tools have you used to enforce MISRA compliance?
What is a deviation? How is it handled in MISRA compliance?
Explain Rule 11.4 (Avoid conversion between pointers to different object types).
Why does MISRA-C++ restrict the use of multiple inheritance and virtual functions?
How would you fix a violation of Rule 8.7 (Functions with internal linkage)?
How do you handle third-party code that violates MISRA guidelines?
Advanced-Level Questions
How would you implement a MISRA compliance process in an existing codebase?
What challenges do you face while using static analysis tools for MISRA?
How do MISRA-C and ISO 26262 relate in terms of functional safety?
What are the implications of using reinterpret_cast in C++ for safety-critical systems?
Explain how MISRA helps in enabling formal verification or model-based design.
Can you give an example of a rule violation that caused a bug or issue in production?
How do you justify a deviation with traceability to requirements?
How do you balance performance optimization and MISRA compliance?
What is the difference between MISRA C:2004 and MISRA C:2012?
Have you ever written custom rules in a static analysis tool to supplement MISRA?
FAQ: MISRA-C / MISRA-C++ Guidelines
1. What is MISRA?
MISRA (Motor Industry Software Reliability Association) is a set of coding guidelines for C and C++ designed to help developers write safe, secure, and portable code—especially in safety-critical industries like automotive, aerospace, and medical devices.
2. What is the difference between MISRA-C and MISRA-C++?
MISRA-C applies to the C programming language (mostly used in embedded systems).
MISRA-C++ applies to the C++ language, offering guidance for object-oriented programming in critical systems.
3. Why should I follow MISRA guidelines?
Following MISRA helps:
Prevent undefined behavior
Improve code reliability and maintainability
Ensure safety and compliance with industry standards (e.g., ISO 26262)
4. Is MISRA only for the automotive industry?
No. Although MISRA was started by the automotive industry, it is now widely adopted in aerospace, medical, railway, industrial control, and defense sectors.
5. Do I need special tools to check for MISRA compliance?
Yes, most developers use static analysis tools like:
PC-lint
Parasoft
Coverity
Helix QAC These tools automatically detect violations of MISRA rules in your code.
6. Are all MISRA rules mandatory?
No. MISRA guidelines include:
Mandatory rules (must always be followed)
Required rules (must be followed unless a formal deviation is documented)
Advisory rules (recommended best practices)
7. Can I use dynamic memory (malloc, free) in MISRA-compliant code?
Generally, MISRA discourages or forbids dynamic memory in embedded systems because of potential issues like fragmentation and non-determinism, especially in real-time systems.
8. How do I start learning MISRA guidelines as a beginner?
Start by:
Understanding basic C/C++ syntax and behavior
Reading MISRA rule categories (e.g., syntax, type safety, portability)
Studying real-world code examples
Using static analysis tools to get hands-on experience
9. What’s covered in this tutorial?
This tutorial covers:
Introduction to MISRA-C and MISRA-C++
Deep dive into key rules with code examples
Step-by-step guidance to write compliant code
Static analysis integration
How to document deviations and avoid common mistakes
10. Do I need to buy the official MISRA documents?
While the official MISRA documents provide complete rule sets and rationale, this tutorial offers simplified, beginner-friendly explanations. However, if you’re working professionally, it’s recommended to purchase the official documentation from the MISRA website.
You can also Visit other tutorials of Embedded Prep
Code Coverage Tools : In this comprehensive and beginner-friendly tutorial, we will explore the world of code coverage tools, which play a crucial role in ensuring your software is thoroughly tested and reliable. Whether you’re working on embedded systems, desktop applications, or large-scale software projects, understanding code coverage is essential for improving software quality and identifying untested parts of your codebase.
By the end of this tutorial, you will have a solid understanding of how code coverage tools work and how to integrate them into your own development workflow. Whether you are a student, a developer in testing, or an embedded systems engineer, this guide will empower you to write better-tested and more robust code in 2025 and beyond.
What Is Code Coverage?
Code coverage is a metric used to measure how much of your source code has been executed (or “covered”) during testing. It helps answer questions like:
Have all functions been tested?
Have all branches or conditions been exercised?
Are there any unused lines of code?
In simple terms, code coverage tools help ensure your tests are doing their job.
Why Code Coverage Matters
✅ Identifies untested parts of code
🚨 Helps find dead code or redundant logic
🔍 Improves software reliability
📊 Provides quality insights for QA, DevOps, and Developers
💬 Useful in code reviews and CI/CD pipelines
Types of Code Coverage Metrics
Type
Description
Example
Line Coverage
% of lines executed
90 out of 100 lines = 90%
Function Coverage
% of functions called
8 of 10 functions = 80%
Branch Coverage
% of if/else branches executed
Both if and else parts
Condition Coverage
% of boolean sub-expressions tested
if (A && B) tests A and B
Path Coverage
% of possible execution paths tested
All loops + conditions
Popular Code Coverage Tools
Tool
Language
Platform
Usage
gcov
C/C++
Linux
Built-in with GCC
lcov
C/C++
Linux
Visual frontend for gcov
gcovr
C/C++
Cross-platform
Outputs XML/HTML
JaCoCo
Java
Cross-platform
Java code coverage
Coverage.py
Python
Cross-platform
Simple and powerful
Istanbul
JavaScript
Node.js
For JS/TS projects
Tutorial: Using gcov and lcov (For C/C++ Beginners)
This tool summarizes all .gcov into HTML – very handy!
Summary
Code coverage tools help validate testing effectiveness.
For C/C++, gcov, lcov, and gcovr are most used.
Compile with coverage flags, execute the binary, then generate reports.
Visual tools help QA, CI, and reviews.
Code coverage != code quality but is a crucial metric.
We’ll:
Write a simple C program.
Compile it with code coverage flags.
Run it to generate coverage data.
Use gcov to view the coverage.
Optionally, use lcov + genhtml to generate an HTML report.
Step-by-Step Code Coverage Example with Code
Step 1: Write the C Program (math_utils.c)
// math_utils.c
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
int divide(int a, int b) {
if (b == 0) {
printf("Division by zero error!\n");
return 0;
}
return a / b;
}
int main() {
printf("Add: %d\n", add(5, 3));
printf("Subtract: %d\n", subtract(5, 3));
// multiply and divide not called
return 0;
}
This creates extra files like math_utils.gcno (for structure data).
Step 3: Run the Program
./math_utils
Output:
Add: 8
Subtract: 2
This execution will generate math_utils.gcda (runtime data file showing which lines ran).
Step 4: Run gcov to See Coverage
gcov math_utils.c
You’ll see something like:
File 'math_utils.c'
Lines executed:60.00% of 15
Creating 'math_utils.c.gcov'
Then open the file math_utils.c.gcov:
-: 1: // math_utils.c
-: 2: #include <stdio.h>
#####: 3:
1: 4: int add(int a, int b) {
1: 5: return a + b;
-: 6: }
1: 7:
1: 8: int subtract(int a, int b) {
1: 9: return a - b;
-: 10: }
#####: 11: int multiply(int a, int b) {
#####: 12: return a * b;
-: 13: }
#####: 14: int divide(int a, int b) {
#####: 15: if (b == 0) {
#####: 16: printf("Division by zero error!\n");
#####: 17: return 0;
#####: 18: }
#####: 19: return a / b;
-: 20: }
1: 21:
1: 22: int main() {
1: 23: printf("Add: %d\n", add(5, 3));
1: 24: printf("Subtract: %d\n", subtract(5, 3));
-: 25: // multiply and divide not called
1: 26: return 0;
-: 27: }
What You Learn From This
Lines marked #####: <line number> are never executed.
Functions not tested = 0% coverage.
You only called add() and subtract() in main() – so only those got executed.
You’ll see a colorful, user-friendly HTML page showing function/line/branch coverage.
Summary
✅ Code coverage tools like gcov and lcov help track which parts of your code were tested. ✅ Use flags -fprofile-arcs -ftest-coverage during compilation. ✅ gcov gives line-by-line coverage in plain text. ✅ lcov + genhtml gives interactive HTML reports. ✅ Helps you spot untested functions (like multiply() and divide() above).
You can also Visit other tutorials of Embedded Prep
Welcome to the Master Beginner-Friendly Guide to the Embedded Boot Process (2025 Edition)! In this tutorial, you’ll gain a clear and practical understanding of how embedded systems start up — from the moment power is applied to the system until the operating system or application is fully running.
What You Will Learn:
What is the Boot Process? Understand what happens behind the scenes when an embedded device powers on.
Stages of the Boot Process:
Power-On Reset (POR)
Boot ROM / Initial Bootloader
Secondary Bootloader (SPL / U-Boot)
Kernel Loading
Root Filesystem Mounting
User Application Launch
Key Components Involved: Learn about bootloaders, memory maps, non-volatile storage (NAND, NOR, eMMC), and how firmware interacts with hardware.
Bare-metal vs. OS-based Booting: Discover the difference in boot flow when using no OS, RTOS, or full-fledged OS like Linux or QNX.
Practical Example: Walk through a real-world boot sequence on popular boards like STM32, BeagleBone Black, or Raspberry Pi.
Debugging Boot Issues: Learn how to use serial consoles, boot logs, and debugging tools to trace boot-time failures.
Tips for Faster and Reliable Booting Optimize your embedded system’s startup time and stability.
Have you ever wondered what happens when you turn on your computer or a device like a mobile phone or embedded board (e.g., Raspberry Pi, Arduino, etc.)?
Let’s break it down step by step, using simple words!
1. Power ON
When you press the power button:
Electricity flows to the motherboard and components.
This is just like waking up your device from sleep.
2. CPU Starts (but knows nothing!)
The CPU (brain of the system) starts running, but it has no idea what to do.
So, it looks for instructions in a special memory called ROM (Read-Only Memory).
3. BIOS / Bootloader (First Instructions)
The CPU finds a tiny program in ROM, like a mini-guidebook. This is:
BIOS (for PCs) or
Bootloader (in embedded systems or phones)
This program:
Checks if important parts like RAM, keyboard, screen, etc., are working.
Then it looks for the main software (Operating System) to run.
4. Loading the Operating System (OS)
Once the check is done:
It loads the OS (like Windows, Linux, Android, or RTOS) from storage (HDD/SSD/Flash) to RAM.
RAM is like a fast-access workspace where the OS can run quickly.
5. OS Takes Over
Now the OS is running, and:
It manages the hardware.
It starts services and programs.
It shows you the login screen or desktop.
Congratulations! 🎉 Your device is now ready to use!
Quick Summary (Like a Flowchart):
1. Power ON 🔌
2. CPU starts 🧠
3. Bootloader/BIOS runs 📦
4. OS is loaded 💿
5. OS takes control and system is ready 🖥️
Real Life Example (Simple Analogy):
Think of your device like a car:
Battery ON → Supplies power
Ignition Key Turned → CPU wakes up
Dashboard Checks → Bootloader checks parts
Engine Starts → OS is loaded
You Can Drive → System ready for use
U-Boot Basics
What is U-Boot?
U-Boot (short for Universal Boot Loader) is an open-source bootloader used in embedded systems. It helps your device start up, initialize hardware, and load the operating system (like Linux, Android, or RTOS).
Why Do We Need U-Boot?
When you power on an embedded board (like a BeagleBone, STM32, or Raspberry Pi):
1. First Stage Bootloader (SPL – Secondary Program Loader)
Very small size, loaded from ROM or flash.
Initializes basic things like DRAM (RAM).
Loads the full U-Boot image from storage into RAM.
2. Second Stage Bootloader (Full U-Boot)
Runs in RAM.
Has full features: CLI, device drivers, environment variables, loading kernel, etc.
U-Boot in Action – Boot Flow
Let’s say you power on your embedded board:
Power ON
↓
ROM Code (fixed in CPU)
↓
SPL (from SD card, flash)
↓
U-Boot (full version loaded into RAM)
↓
Kernel (Linux, RTOS, etc.)
↓
Root Filesystem
↓
Your Application
Common U-Boot Commands
You interact with U-Boot via a command-line interface (serial terminal like PuTTY, Minicom, TeraTerm).
Here are some commonly used U-Boot commands:
Command
Description
help
List all commands
printenv
Show environment variables
setenv var value
Set environment variable
saveenv
Save variables to flash
boot
Boot the OS
load
Load file into memory
mmc, fatload, ext4load
Load files from SD card (FAT or EXT4)
bootm
Boot a Linux kernel image (uImage)
bootz
Boot a zImage
booti
Boot an Image format (used for ARM64)
run
Run a command script or bootcmd
U-Boot Environment Variables
These are key-value pairs that control how the board boots.
git clone https://source.denx.de/u-boot/u-boot.git
cd u-boot
Step 2: Select Board
make <board_defconfig> # e.g., make am335x_evm_defconfig
Step 3: Build U-Boot
make CROSS_COMPILE=arm-linux-gnueabihf- # or your toolchain prefix
Output Files
u-boot.img – U-Boot image
MLO – SPL (for some boards like BeagleBone)
Flashing U-Boot to Board
SD Card: Place MLO and u-boot.img in boot partition.
Flash Memory: Use tools like dd, flashcp, or manufacturer utilities.
Tips for Beginners
Always backup your current U-Boot before changing it.
Use serial console to access U-Boot.
Keep a bootable SD card as recovery in case of a bad flash.
Learn U-Boot environment—it’s key to customization.
Customizing U-Boot for Embedded Systems
Customizing U-Boot means modifying its behavior to suit your specific board, hardware, or use case — like setting boot arguments, adding splash screens, or enabling new peripherals.
Let’s walk through it step by step 👇
1. Prerequisites
Before customizing, make sure you have:
✅ A Linux PC or VM ✅ A toolchain (like arm-linux-gnueabihf-gcc) ✅ U-Boot source code ✅ Board information (CPU, RAM, Flash type) ✅ Serial communication tool (like Minicom or PuTTY)
2. Get the U-Boot Source Code
git clone https://source.denx.de/u-boot/u-boot.git
cd u-boot
3. Choose the Right Board Configuration
U-Boot uses defconfig files for board settings.
To find yours:
ls configs/
Pick your board (example: BeagleBone Black)
make am335x_evm_defconfig
For custom boards, copy a similar defconfig and modify it.
4. Build U-Boot
export CROSS_COMPILE=arm-linux-gnueabihf-
make -j$(nproc)
mkimage -A arm -T script -C none -n "Boot Script" -d boot.cmd boot.scr
Place boot.scr on SD card — U-Boot will run it automatically.
9. Tips for Custom Boards
Use similar board’s config as starting point.
Add support for your DRAM, clock, storage in board.c and dts.
Customize dts (Device Tree Source) file for Linux as well.
Summary
What You Did
Description
Got U-Boot source
git clone
Chose board config
make defconfig
Customized features
Edited header files, bootcmd
Enabled features
Splash screen, drivers
Rebuilt & flashed
make, dd
Tested via serial
Used setenv, boot
(Optional) Used script
Boot automation with boot.scr
Bootloader Initialization in Embedded Systems
If you’ve ever wondered how your embedded device starts running code, the answer is: it all begins with the bootloader.
Let’s break down what a bootloader is, how it works, and how it initializes your system — step by step.
What Is a Bootloader?
A bootloader is the first program that runs when your embedded system powers up or resets. It prepares your hardware and loads the main firmware (like Linux or RTOS).
Think of it like:
“A mini operating system that wakes up your device, checks everything, and hands control to the main software.”
Why Bootloader Initialization Matters?
Before your firmware (or OS) can run, the hardware needs to be set up properly:
Clock signals must be configured ✅
Memory (RAM, Flash) should be initialized ✅
Peripherals like UART, SPI, etc. must be ready ✅
Bootloader Initialization Step-by-Step
Here’s what typically happens:
1. Reset or Power-On
The microcontroller or processor powers up.
The Program Counter (PC) is set to a fixed boot address.
This points to the bootloader’s start location (usually in Flash memory).
2. Execution Begins: Start.S or crt0.S
The CPU starts running assembly code.
This code does:
Disabling interrupts
Setting up the stack pointer
Copying data from Flash to RAM
Clearing the .bss section (zero-initialized variables)
This is known as the low-level initialization stage.
3. Hardware Initialization (Board-Level)
Now, the bootloader code written in C takes over (often in board_init() or main() function).
It does:
Configures CPU clock frequency
Initializes UART (for printing messages)
Initializes SDRAM (if external)
Sets up power management (PMIC)
Prepares GPIOs, I2C, SPI, etc.
This code is often board-specific and may live in board.c or init.c in U-Boot.
4. Detect and Load Firmware
The bootloader then:
Detects the storage device (eMMC, SD card, SPI flash, etc.)
Loads the main firmware (like zImage, uImage, or RTOS binary) into RAM
Sets up boot arguments (bootargs)
Jumps to the entry point of the firmware
Think of this as: “Here’s your OS. Time to run it!”
5. Hand-Off to OS
Finally, the bootloader does a jump to the firmware:
In custom bootloaders: You write it from scratch or using templates.
In U-Boot: It’s split into files like start.S, board.c, init.c, etc.
In microcontrollers: It’s often written in startup.s, main.c, and linked via linker script.
Tools Needed
Tool
Purpose
Cross Compiler (like arm-none-eabi-gcc)
To build bootloader
Serial Console (Minicom, PuTTY)
To see logs via UART
Flashing Tool (dd, OpenOCD)
To write bootloader to flash
Summary
Bootloader Stage
Action
Power-On
CPU starts from fixed address
Low-level Init
Stack, memory, .bss, vectors
Board Init
Clock, UART, RAM, peripherals
Load Firmware
Read from Flash or SD
Jump to OS
Handoff control
Final Words
Bootloader initialization is the foundation of system bring-up. If something goes wrong here, nothing else works.
Start simple, log every step, and understand what your hardware needs to “wake up.”
Here’s a Beginner-Friendly Guide to Secure Boot — explained step-by-step for someone with zero background in security or bootloaders.
Beginner-Friendly Tutorial: What is Secure Boot?
Why This Matters?
Imagine your embedded device is like a home. Would you want just anyone to unlock your door and change how it works? No! That’s where Secure Boot comes in.
Secure Boot makes sure that only trusted software can run when your embedded system starts. It protects against malware, tampering, and unauthorized code.
What is Secure Boot?
Secure Boot = A locked gate that only opens for verified, trusted software.
It’s a security feature in bootloaders (like U-Boot) and processors. Before booting the main firmware or OS, Secure Boot checks the signature of the software image. If it’s valid (not tampered), it boots. Otherwise, it stops.
Simple Analogy
Think of it like scanning a QR code to enter a room. If the code is valid and signed, you’re in. If someone changes the QR code, the scanner refuses access.
How Does Secure Boot Work? (Step-by-Step)
Step 1: Key Generation
A private/public key pair is created using cryptography (like RSA or ECC).
Private Key → Secret (kept by the manufacturer)
Public Key → Stored in the device (in hardware or ROM)
Step 2: Signing the Firmware
The firmware (like a Linux image or RTOS) is hashed and then digitally signed with the private key.
This creates a signature file attached to the firmware.
Step 3: Burning the Public Key
The public key is stored in a trusted area of the device (like One-Time Programmable (OTP) memory or eFuse).
This makes it impossible to change later (hardware enforced).
Step 4: Secure Boot Process
When the system powers up:
Boot ROM (or Primary Bootloader) is executed from secure memory.
It reads the firmware (e.g., u-boot.img, zImage).
It verifies the signature using the stored public key.
If valid → The boot continues.
If invalid → Boot is aborted (or recovery mode is triggered).
U-Boot will now verify the signature before booting!
What Happens If Tampered?
If someone modifies the kernel or DTB:
U-Boot’s signature verification will fail
The boot will be aborted
You’ll see a message like:
Error: verification failed for 'kernel@1'
Bonus: Secure Boot Chain
Stage
Verified?
Boot ROM (SoC)
Yes – hardwired
SPL (Secondary Bootloader)
Yes – optional signature
U-Boot Proper
Yes
Linux Kernel (FIT image)
Yes
Root Filesystem
No (unless additional mechanisms used like dm-verity)
Want to try on STM32 or NXP i.MX?
Each vendor has slight variations:
STM32 uses OEM keys, STM32CubeProgrammer, and Secure Boot Services
NXP i.MX uses AHAB, CST tool, and fuse-based key provisioning
I can guide you through those too if needed.
Bootloader for Flash Memory
When working with embedded systems, one crucial component that ensures your system boots correctly is the Bootloader. The Bootloader for Flash Memory is responsible for loading the operating system (or kernel) from flash storage to RAM when the system powers up.
What is a Bootloader?
A Bootloader is small software that runs when a system is powered on. It’s responsible for initializing the hardware and loading the main operating system (OS) or kernel.
When the system starts, it first checks if the bootloader is present in flash memory. If so, it loads and runs the bootloader. The bootloader then locates the kernel in the flash memory and transfers control to it.
Flash Memory Overview
Flash memory is a type of non-volatile storage, which means it retains data even after power is lost. It’s commonly used to store the bootloader, operating system, and sometimes user data.
Two Key Types of Flash Memory:
NOR Flash – Faster read speeds, slower writes, often used for storing the bootloader and firmware.
NAND Flash – Slower reads, faster writes, typically used for storing large amounts of data (like operating systems).
How Does the Bootloader Work with Flash Memory?
When the system powers up, the processor or microcontroller will follow this sequence:
1. Power-Up / Reset:
The CPU starts in a known state from ROM (Read-Only Memory).
2. Boot ROM:
The Boot ROM (part of the processor) is the very first code that runs. It may contain a simple routine to check if a valid bootloader is present in the flash memory.
3. Loading Bootloader:
The Boot ROM will read the bootloader from flash memory (e.g., NOR Flash) and load it into RAM.
4. Bootloader Execution:
The Bootloader then executes, initializing the system and setting up hardware like memory, peripherals, and communication interfaces.
5. Loading the Operating System:
Once the hardware is initialized, the bootloader locates the kernel (or OS image) stored in the flash memory, loads it into RAM, and then transfers control to the kernel.
Flash Memory as Boot Storage
In most embedded systems, the bootloader resides in Flash memory. Flash memory has both read and write capabilities, allowing it to store the bootloader and kernel in a way that survives resets or power cycles.
How to Set Up a Bootloader for Flash Memory
Here’s a simple overview of how you can create and set up a bootloader for your system that uses flash memory:
1. Choose the Flash Memory Type
Decide whether your system uses NOR or NAND flash.
NOR Flash is often used for bootloaders because it allows faster reading, making it ideal for the first step in the boot process.
2. Configure the Bootloader for Flash Memory
U-Boot is one of the most commonly used bootloaders for embedded systems. You need to configure it to work with your flash memory type (NOR/NAND).
For example, U-Boot allows you to configure it with:
CONFIG_SYS_FLASH_BASE for NOR flash
CONFIG_SYS_NAND_BASE for NAND flash
These configurations tell the bootloader where to read from in the flash memory.
3. Prepare Flash Memory for Bootloader
Flash Programming: You’ll need to program the flash memory with your bootloader and OS image.
Flashing tools like JTAG, USB-to-Serial, or a flasher can help in writing to flash memory.
4. Create the Bootloader Image
Once U-Boot (or any other bootloader) is configured, you need to create an image that can be written to flash.
Example U-Boot image creation: make CROSS_COMPILE=arm-linux-gnueabihf- u-boot.img
5. Flash the Bootloader
After compiling the bootloader image (u-boot.img), you can use tools to write it into the flash memory.
After writing the bootloader into flash memory, power on the system and watch the boot process to verify if the system boots correctly from flash.
Simple Boot Process Using Flash Memory:
Here’s a very simplified sequence of the boot process when using flash memory:
Power-On → Boot ROM → Load Bootloader (from Flash) → Bootloader Executes → Load Kernel (from Flash) → Boot OS
In Detail:
Power-On: System powers up, and the CPU starts from ROM.
Boot ROM: The Boot ROM checks for valid bootloader code in the flash memory.
Bootloader Loads: The bootloader (e.g., U-Boot) is loaded into RAM.
Bootloader Initialization: The bootloader configures hardware and prepares the system for loading the OS.
Load Kernel: The bootloader reads the kernel image from flash and loads it into RAM.
OS Control: The bootloader transfers control to the kernel, and the operating system begins running.
Flash Memory and Security:
To ensure that the boot process is secure and unmodified, you can implement Secure Boot. This ensures that only trusted bootloader and kernel images are loaded from flash memory. It typically involves digital signatures to verify the integrity of the bootloader and OS image.
Real Example with U-Boot for Flash Memory:
Let’s walk through a basic process of using U-Boot to load the OS from flash memory:
1. Prepare U-Boot Configuration
In your u-boot/configs directory, configure the board for flash memory:
make am335x_evm_defconfig
Modify the U-Boot configuration for flash memory access (for example, for NAND):
make menuconfig
# Enable NAND and configure memory
2. Build U-Boot
Now, build the U-Boot image for your system:
make CROSS_COMPILE=arm-linux-gnueabihf- u-boot.bin
3. Flash U-Boot to NAND/NOR
Use a flashing tool to program U-Boot to the flash memory:
sudo dd if=u-boot.bin of=/dev/nand1 bs=512 seek=1
4. Verify Boot Process
Once U-Boot is installed, power on the device. It should automatically load the OS (or kernel) from flash memory.
Frequently Asked Questions (FAQs)
Q: What is the difference between U-Boot and BIOS? A: BIOS is for PCs; U-Boot is for embedded systems. Both initialize hardware and boot the OS.
Q: Can U-Boot run scripts? A: Yes, using environment variables and run command.
Q: Is U-Boot customizable? A: Yes! It’s open source, so you can add/remove features and drivers.
Key Takeaways:
Bootloaders load your OS/kernel from flash memory.
Flash Memory is non-volatile, making it perfect for boot storage.
U-Boot is commonly used as a bootloader for embedded systems.
Secure Boot adds authentication to make sure only trusted code is run.
You can also Visit other tutorials of Embedded Prep
SDLC Software Development Life Cycle : The Software Development Life Cycle (SDLC) is a step-by-step process used to build software efficiently. Think of it like a recipe 📜 — it guides you from the idea stage to a fully cooked (working!) application.
SDLC Software Development Life Cycle
1. Requirements Analysis
Goal: Understand what the software needs to do.
In this phase, developers talk to clients or users to find out:
What problem the software should solve
What features are needed
Who will use it, and how
Deliverable: A document that lists clear software requirements.
Tip: Imagine you’re building an app like a food delivery service. This phase figures out: Do users need real-time tracking? How should payment work?
2. Design Phase
Goal: Plan how the software will work internally and externally.
📊 Based on the requirements, software architects and developers:
Design the system structure (modules, databases, UI layout)
Choose the technologies (programming languages, tools)
🖼️ It’s like creating blueprints before constructing a building.
Work is divided into small cycles (sprints), and each cycle delivers a usable part of the product.
Plan ➡ Design ➡ Develop ➡ Test ➡ Review ➡ Repeat (for each sprint)
Pros:
Flexible and adaptive to change
Continuous feedback and improvement
Faster delivery of features
Cons:
Needs close communication with clients
Requires experienced teams to manage sprints well
Summary Table SDLC Software Development Life Cycle
Feature
Waterfall Model
Agile Model
Approach
Linear (one-time flow)
Iterative (repeats)
Flexibility
Low
High
Customer Involvement
At the beginning only
Ongoing, frequent
Best For
Simple, fixed-scope projects
Evolving, complex projects
Delivery Style
One big release
Small, regular releases
Conclusion SDLC Software Development Life Cycle
The SDLC helps developers stay organized and efficient when building software. Whether you’re working on a simple school project or a large enterprise system, understanding SDLC is essential.
Start with requirements
Move through design and implementation
Carefully test and maintain
Choose the right model: Agile for flexibility, Waterfall for simplicity
The Agile Model is a software development methodology that emphasizes iterative development, customer collaboration, responding to change, and frequent delivery of working software. Unlike traditional waterfall models, Agile does not follow a strict sequence of phases like
it divides the project into small iterations or sprints where all activities (like design, coding, testing) happen in parallel.
Overview of the Agile Model
Agile is adaptive, not predictive. It follows the Agile Manifesto, which emphasizes:
Individuals and interactions over processes and tools
Working software over comprehensive documentation
Customer collaboration over contract negotiation
Responding to change over following a plan
Agile isn’t a single process but a set of principles and values. However, it is implemented using frameworks like Scrum, Kanban, Extreme Programming (XP), etc.
Core Phases / Processes of the Agile Model
Although Agile is iterative and phases often overlap, it still follows a cycle in each iteration or sprint.
1. Concept / Requirement Gathering
Goal: Understand what the customer wants.
Process:
Stakeholders describe high-level features (called epics or user stories).
Product owner adds these to the product backlog.
Tools: User Story format (As a [user], I want [feature] so that [benefit]), JIRA, Trello.
2. Planning (Sprint Planning)
Goal: Plan what can be delivered in the upcoming sprint (usually 1-4 weeks).
Process:
Product Owner prioritizes user stories.
Team selects stories they can commit to during the sprint.
Tasks are broken down and estimated (e.g., in story points or hours).
3. Design
Goal: Quickly plan the architecture/design of selected features.
Process:
Collaborative design with minimal upfront effort.
Use diagrams (e.g., UML), whiteboards, or design tools.
Note: Agile supports evolutionary design—it evolves as development proceeds.
4. Development / Implementation
Goal: Write working code that meets the sprint goal.
Process:
Developers start coding as per stories/tasks.
Code is regularly pushed, integrated (often daily), and unit-tested.
Continuous Integration (CI) pipelines are often used.
5. Testing
Goal: Ensure the functionality meets the acceptance criteria.
Process:
Testers write test cases for each user story.
Conduct functional testing, regression testing, and sometimes automated testing.
Defects found are resolved within the same sprint.
6. Deployment / Release
Goal: Deliver working software to users or staging environments.
Process:
Software can be released at the end of every sprint (if shippable).
Use DevOps practices like CI/CD for faster releases.
7. Review (Sprint Review)
Goal: Demonstrate completed features to stakeholders.
Process:
Team presents what was completed.
Stakeholders provide feedback.
Feedback is noted and can become new user stories.
8. Retrospective
Goal: Improve the process for future sprints.
Process:
Team discusses what went well, what didn’t, and what can be improved.
Action items are recorded and applied in the next sprint.
Agile Iteration Cycle Summary
User Story → Sprint Planning → Design → Development → Testing → Sprint Review → Retrospective → Next Sprint
Each sprint is time-boxed and results in a potentially shippable product increment.
Key Agile Terms SDLC Software Development Life Cycle
Term
Description
User Story
A feature described from the user’s perspective.
Sprint
A fixed-duration iteration (usually 1–4 weeks).
Backlog
A prioritized list of user stories.
Scrum Master
Facilitates Scrum practices and removes blockers.
Product Owner
Owns the product vision and backlog.
Daily Standup
A 15-minute meeting to sync up.
Burndown Chart
Shows remaining work in a sprint.
Benefits of Agile
Fast delivery of features
Flexibility to changes
Continuous user feedback
High product quality
Encourages teamwork and transparency
When Agile May Not Be Suitable
Projects with very fixed scope and budget
Safety-critical or heavily regulated systems (unless Agile is tailored carefully)
Teams unfamiliar with Agile principles
Project Management & Collaboration
Jira – Widely used for Scrum/Kanban boards, user stories, sprints, and burndown charts.
Trello – Visual task management using boards and cards (popular for small teams).
Asana – Task tracking with project timelines and collaboration.
ClickUp – Combines tasks, docs, and goals in one platform.
VersionOne – Agile project management tool supporting SAFe and Scrum.
Azure DevOps (formerly VSTS) – Integrated with Microsoft tools for Agile planning and CI/CD.
Source Control & Code Management
Git – Distributed version control system.
GitHub / GitLab / Bitbucket – Platforms for Git repositories with built-in issue tracking and CI/CD.
CI/CD & Build Automation
Jenkins – Automates builds and deployments.
CircleCI / Travis CI / GitLab CI – Integrates with Git to run automated pipelines.
TeamCity – JetBrains tool for continuous integration and delivery.
Testing Tools
Selenium – Web application automation testing.
JUnit / NUnit / TestNG – Unit testing frameworks.
Postman – API testing.
Cucumber – BDD tool for writing test cases in plain English.
QTest – Test case management.
Documentation & Knowledge Sharing
Confluence – Wiki-style documentation often paired with Jira.
Notion – All-in-one workspace for docs, tasks, and wikis.
Google Docs/Sheets – Lightweight collaboration tools for real-time editing.
Monitoring & Feedback
Burndown/Burnup Charts – Usually generated within tools like Jira.
Retrospective Tools – EasyRetro, FunRetro for sprint reviews.
Slack / Microsoft Teams – Team communication and real-time updates.
Let’s include IBM tools that are widely used in Agile and systems engineering environments, especially in regulated industries like automotive, aerospace, and healthcare.
🏢 IBM Tools in Agile Methodology
IBM Jazz Platform
A collaborative lifecycle management (CLM) platform designed for Agile and DevOps:
IBM Engineering Workflow Management (EWM)
Formerly: Rational Team Concert (RTC)
Used for: Agile planning, work item tracking, SCM, and builds.
Features: Sprint planning, Kanban boards, task tracking, and integrations.
IBM Engineering Requirements Management DOORS Next (DNG)
Formerly: DOORS Next Generation
Used for: Requirements elicitation, management, and traceability.
Features: Collaboration on requirements in Agile teams, linking to user stories, test cases, and code.
IBM Engineering Requirements Management DOORS (Classic DOORS)
Used for: Hierarchical, structured requirements in complex systems.
Features: Traceability, baselining, and change control.
Still widely used in large legacy and safety-critical projects.
IBM Engineering Test Management (ETM)
Formerly: Rational Quality Manager (RQM)
Used for: Test planning, execution, and linking to requirements and development tasks.
IBM Engineering Lifecycle Optimization – Engineering Insights (ENI)
Used for: Visualizing relationships between artifacts like requirements, code, and test cases.
These IBM tools are tightly integrated and support Agile at scale, traceability, compliance, and complex product development lifecycles, especially when following frameworks like SAFe or ASPICE.
FAQ: Software Development Life Cycle (SDLC)
1. What is SDLC? Answer: SDLC (Software Development Life Cycle) is a structured process used by software developers and project teams to design, develop, test, deploy, and maintain software applications. It ensures the quality and correctness of software.
2. What are the main phases of SDLC? Answer: The typical SDLC phases are:
Requirement Gathering & Analysis
System Design
Implementation (Coding)
Testing
Deployment
Maintenance
3. Why is SDLC important? Answer: SDLC provides a clear framework that helps ensure systematic development, minimizes risks, ensures quality, and delivers projects on time and within budget.
4. What are some common SDLC models? Answer: Common SDLC models include:
Waterfall Model
Agile Model
V-Model (Validation and Verification Model)
Spiral Model
Iterative Model
Big Bang Model
5. What is the difference between Waterfall and Agile? Answer:
Waterfall is linear and sequential; each phase must be completed before the next.
Agile is iterative and flexible; development is done in small sprints with regular feedback and continuous improvement.
6. What happens in the Requirement Gathering phase? Answer: In this phase, business analysts and stakeholders gather and document the functional and non-functional requirements of the system.
7. What does the Design phase include? Answer: The design phase includes system architecture, data models, user interfaces, and technical specifications. It serves as a blueprint for development.
8. What is the role of testing in SDLC? Answer: Testing ensures that the software meets requirements and is free from defects. It includes unit testing, integration testing, system testing, and user acceptance testing (UAT).
9. How is maintenance handled in SDLC? Answer: After deployment, software needs regular updates, bug fixes, security patches, and performance improvements. This is managed in the maintenance phase.
10. What tools are commonly used in SDLC? Answer:
Requirement Management: JIRA, Confluence
Design: UML tools, Draw.io
Development: IDEs like VS Code, IntelliJ
Testing: Selenium, JUnit, TestNG
Version Control: Git, GitHub
CI/CD: Jenkins, GitLab CI
11. Who are the key participants in SDLC? Answer: Stakeholders include Business Analysts, Project Managers, Developers, Testers, DevOps Engineers, and Customers.
12. Can SDLC be used in embedded software development? Answer: Yes, SDLC applies to embedded systems as well, with adaptations for hardware constraints, real-time requirements, and integration with physical devices.
You can also Visit other tutorials of Embedded Prep
Git Basics : In this blog post, we will dive deep into the core concepts of Git, one of the most widely used version control systems in modern software development. Whether you’re a beginner or looking to brush up on Git, this guide will help you understand how to efficiently manage and track your codebase.
We’ll start by covering the Git Basics such as cloning repositories, committing changes, pushing and pulling updates, and how these foundational commands are used in everyday development. After that, we’ll explore the power of Branching and Merging, which enables parallel development on separate features or fixes without disrupting the main codebase.
Next, we will dive into Git Workflow and examine popular strategies like Feature Branch Workflow and Git Flow, and explore the commands necessary to follow these workflows effectively. We’ll also tackle Conflict Resolution — a common challenge when collaborating with others — and provide best practices for resolving merge conflicts.
Furthermore, we will compare Git Rebase vs Merge, and understand their differences, use cases, and impact on project history. Finally, we will wrap up with Git Tags and Releases, showing how to mark specific points in your project’s history, such as version releases, and how to manage them with tags.
By the end of this post, you’ll have a solid understanding of Git’s key features and commands, enabling you to collaborate smoothly in any software development project.
1. Git Basics Overview:
Git is a distributed version control system that helps developers manage and track changes to their codebase over time. It allows multiple users to collaborate on the same project without overwriting each other’s changes. Below are the basic Git commands that are commonly used in any development project.
Clone: git clone is used to create a local copy of a remote repository. This command pulls down all the files, commit history, and branches from the remote repository to your local machine. git clone <repository_url> Example: git clone https://github.com/user/project.git
Commit: A commit represents a snapshot of your changes. After modifying files in your working directory, you need to add them to staging and commit them to the local repository.
Stage files: git add <file> adds specific files to the staging area. To add all changed files, use git add .. git add .
Commit changes: Once your files are staged, you use the git commit command to save your changes to the local repository. git commit -m "Commit message describing changes"
Push: The git push command is used to upload local repository content to a remote repository. It is typically used after committing changes locally to push them to a shared remote repository. git push origin <branch_name> Example: git push origin main
Pull: git pull is used to fetch and integrate changes from the remote repository into your local branch. This command combines git fetch and git merge, ensuring you have the latest changes from the remote branch. git pull origin <branch_name> Example: git pull origin main
Branching & Merging
2. Branching:
Branching allows you to create separate environments to work on different features or fixes without affecting the main codebase. Each branch can be developed independently and then merged back into the main branch.
Create a branch: You can create a new branch with the git branch command. git branch <branch_name> Example: git branch feature/new-feature
Switch to a branch: To start working on a branch, use git checkout to switch to it. git checkout <branch_name> Example: git checkout feature/new-feature Alternatively, you can combine both creating and switching with: git checkout -b <branch_name> Example: git checkout -b feature/new-feature
3. Merging:
Merging is the process of bringing together changes from different branches. Typically, changes from a feature branch are merged back into the main branch.
Merge a branch: First, ensure you’re on the branch that you want to merge into (typically main or master), then use the git merge command. git merge <branch_name> Example: git merge feature/new-feature
Git Workflow (Feature Branch, Git Flow) and All Commands
4. Git Workflow:
Git workflow refers to how developers manage their branches and commits within a repository. Two popular workflows are the Feature Branch workflow and Git Flow workflow.
Feature Branch Workflow: In this workflow, each new feature or bug fix is developed in a separate branch. When the feature is complete, it’s merged into the main branch. Steps:
Create a feature branch: git checkout -b feature/awesome-feature
Open a pull request (PR) and merge into the main branch.
Git Flow: Git Flow is a more structured branching model, often used in larger projects. It divides work into different branches for features, releases, and hotfixes.
Main branches:
main (or master): Production-ready code.
develop: Ongoing development code, where new features are integrated.
Supporting branches:
Feature branches: Used for new features.
Release branches: Prepare for production releases.
Hotfix branches: Used to fix urgent issues in production.
Git Flow commands: Install git-flow: brew install git-flow Initialize git flow in a repository: git flow init Start a feature: git flow feature start <feature_name> Finish a feature: git flow feature finish <feature_name> Start a release: git flow release start <release_version> Finish a release: git flow release finish <release_version> Start a hotfix: git flow hotfix start <hotfix_version> Finish a hotfix: git flow hotfix finish <hotfix_version>
💛 Support Embedded Prep
If you find our tutorials helpful and want to support our mission of sharing high-quality embedded system knowledge, you can contribute by buying us a coffee.
Every small contribution helps us keep creating valuable content for learners like you. ☕
Thank you for your support — it truly keeps Embedded Prep growing. 💻✨
Conflict Resolution
5. Conflict Resolution:
When merging branches, you might encounter conflicts if two branches modify the same part of a file. Git cannot automatically decide which change should be kept, so it marks the conflict in the file.
Resolve Conflicts: When a conflict occurs, Git will mark the file with conflict markers. Open the conflicted file, look for the markers (e.g., <<<<<<<, =======, >>>>>>>), and manually resolve the conflict by selecting or merging the appropriate changes.
After resolving conflicts: Once you’ve resolved the conflict, stage the file and commit the merge. git add <conflicted_file> git commit
Git Rebase vs Merge
6. Git Rebase vs Merge:
Merge: When you use git merge, you combine the histories of two branches into a single branch. A merge commit is created to maintain the history of both branches.
Pros: Maintains a full history of commits, which can be useful for tracking changes.
Cons: The history can become messy with multiple merge commits.
git merge <branch_name>
Rebase: git rebase moves or “replays” your commits from one branch onto another. It results in a cleaner history without merge commits.
Pros: Creates a linear history.
Cons: Can rewrite history, which might be problematic for shared branches.
git rebase <branch_name>
Git Tags and Releases
7. Git Tags and Releases:
Git tags are used to mark specific points in history, often used for releases. A tag points to a particular commit, allowing you to easily identify and reference a release.
Create a tag: Tags are typically created at significant points in a project’s development, like when releasing a version. git tag -a <tag_name> -m "Tag message" Example: git tag -a v1.0 -m "First official release"
Push tags: By default, tags are not pushed to remote repositories. You need to explicitly push tags. git push origin <tag_name> Example: git push origin v1.0
List tags: To view all the tags in a repository: git tag
Checkout a tag: To checkout a specific tag (e.g., a previous release): git checkout <tag_name> Example: git checkout v1.0
Releases: Releases in GitHub or GitLab use tags to provide a versioned snapshot of the code. When creating a release, you can associate it with a tag to specify the version being released.
Step 1: Set up a Remote Repository
To start, we’ll assume there’s a Git repository already hosted on a platform like GitHub or GitLab (you can create one there if needed).
For this example, the URL for the remote repository is:
https://github.com/username/SampleProject.git
Step 2: Clone the Repository
First, we’ll clone the remote repository to create a local copy.
After this, a new folder SampleProject will be created, and it will contain the files from the remote repository.
Step 3: Create a New Branch for Feature Development
Now that we have the repository, let’s create a new branch to work on a new feature. We’ll call this branch feature/add-new-feature.
cd SampleProject
git checkout -b feature/add-new-feature
The -b flag creates and checks out the new branch.
Step 4: Modify Files and Commit Changes
Let’s assume we add a new file or modify an existing one. For this example, we’ll modify a file called README.md.
Open README.md and add a line like: This is a new feature added to the project.
After saving the file, we need to stage the changes and commit them.
git add README.md # Stage the file
git commit -m "Add new feature description to README"
This command stages the changes and commits them with a descriptive message.
Step 5: Push the Branch to Remote Repository
Now that the feature is ready, let’s push the feature/add-new-feature branch to the remote repository.
git push origin feature/add-new-feature
This uploads your local branch to the remote repository, making it available for collaboration or review (such as in a pull request).
Step 6: Create a Pull Request (Optional)
If you’re working with a team, you would now go to the remote Git hosting platform (e.g., GitHub) and create a Pull Request (PR) to merge feature/add-new-feature into the main branch.
Step 7: Pull Latest Changes from the Remote Repository
While working on your feature, others may have pushed changes to the remote repository. To keep your local repository up to date, you can pull the latest changes from the main branch.
git checkout main # Switch to the main branch
git pull origin main # Pull latest changes from remote main branch
Step 8: Merge the Feature Branch into Main
Once the feature is approved (e.g., through a PR review), you can merge it back into the main branch. If you have already completed the PR, you’d merge the branches from the Git hosting platform interface. However, let’s do it manually on the command line:
git checkout main # Switch to the main branch
git merge feature/add-new-feature # Merge feature branch into main
At this point, your main branch includes the changes from feature/add-new-feature.
Step 9: Resolve Merge Conflicts (If Any)
If changes in the main branch and feature/add-new-feature branch conflict (e.g., both modified the same line in README.md), Git will mark the file as conflicted, and you’ll need to resolve the conflict.
You’ll see conflict markers in the file, like:
<<<<<<< HEAD
This is the content from the main branch.
=======
This is the content from the feature branch.
>>>>>>> feature/add-new-feature
You manually edit the file to keep the desired changes and remove the markers.
GCC Command Line Options : Are you new to GCC and feeling overwhelmed by flags like -O2, .o files, or linker scripts? This beginner-friendly tutorial will guide you step-by-step through the world of GCC — the GNU Compiler Collection. Learn how your code transforms from .c to executable, understand every compilation stage (Preprocessing, Compilation, Assembly, Linking), and get hands-on with cross-compilation, object files, static vs shared libraries, and optimization techniques.
You’ll also explore how to debug effectively using -g, what linker scripts do, and how to use essential toolchain components like as, ld, objdump, nm, and readelf.
Perfect for embedded programmers, systems developers, or any curious C/C++ learner who wants to master the build process from the inside out!
What You’ll Learnin this tutorial of GCC Command Line Options:
GCC Command-Line Options Explained
Compilation Stages (Preprocessing ➝ Linking)
Cross-Compilation Made Easy
Understanding .o, .a, .so Files
Optimization Flags (-O0 to -O3, -Os, -Ofast)
Debugging with -g and GDB
Basics of Linker Scripts
Practical Use of as, ld, objdump, nm, readelf
What is GCC?
GCC is a powerful compiler used to compile programs written in C, C++, and other languages. It’s used on Linux, embedded systems, and more.
Cross compilation is when you compile code on one system (host) but the output is meant to run on a different system (target), often with a different CPU architecture (e.g., x86 host → ARM target).
Why Use Cross Compilation?
Building for embedded systems (e.g., ARM Cortex on Raspberry Pi, ESP32)
Developing for devices with limited resources
Compiling code for different operating systems (Linux → QNX, Windows → Linux)
Common Cross Compilation Toolchain
Tool
Purpose
arm-none-eabi-gcc
GCC for bare-metal ARM targets
arm-linux-gnueabihf-gcc
GCC for Linux ARM hard-float ABI targets
aarch64-linux-gnu-gcc
GCC for 64-bit ARM Linux targets
x86_64-w64-mingw32-gcc
GCC for compiling Windows binaries from Linux
Structure of a Cross-Compiler:
<target>-<tool>
Example:
arm-linux-gnueabihf-gcc = cross-compiler for ARM Linux hard-float
Cross Compilation Example:
Let’s say you have this simple C file: main.c
#include <stdio.h>
int main() {
printf("Hello from ARM!\n");
return 0;
}
Compile for ARM Linux:
arm-linux-gnueabihf-gcc main.c -o main_arm
Result:
Output binary main_arm runs on ARM target (e.g., Raspberry Pi)
It will not run on your x86 machine
💛 Support Embedded Prep
If you find our tutorials helpful and want to support our mission of sharing high-quality embedded system knowledge, you can contribute by buying us a coffee.
Every small contribution helps us keep creating valuable content for learners like you. ☕
Thank you for your support — it truly keeps Embedded Prep growing. 💻✨
Use time ./main_Ox or size main_Ox to compare runtime and binary size.
Inspect Optimized Code:
gcc -S -O3 main.c -o main_O3.s
This outputs assembly code so you can see how optimization changes instructions.
Pro Tip:
Use optimization with debugging (covered next):
gcc -g -O2 main.c -o main
This allows debugging even with optimized code, although some variables might be removed or reordered.
Great! Let’s now cover:
Debug Flags & Symbols: -g, -O0 to -O3
When developing software, debugging is crucial. GCC provides flags to include debug symbols and control optimization levels that affect debugging behavior.
-g: Add Debug Info
Embeds debugging symbols (like variable names, function names, source lines) into the binary.
These symbols are used by GDB or other debuggers.
Doesn’t affect performance or behavior at runtime.
Example:
gcc -g main.c -o main
Now you can debug with:
gdb ./main
Optimization & Debugging
Flag combo
Use case
-g -O0 (default)
Easiest debugging: variables, breakpoints work as expected
-g -O2 or -g -O3
Debug optimized code: might skip or inline functions, remove variables
-O2 only
No debug info, faster execution
-g only
Debug symbols, but no optimization
Test Code:
#include <stdio.h>
int main() {
int x = 42;
printf("x = %d\n", x);
return 0;
}
Compile and debug:
gcc -g -O0 main.c -o debuggable
gdb ./debuggable
Inside GDB:
(gdb) break main
(gdb) run
(gdb) print x
If you compile with -O2 or -O3, x may be optimized away:
gcc -g -O2 main.c -o optimized_debug
Inside GDB:
(gdb) print x
May return “optimized out”.
Common Debug Tools:
Tool
Purpose
gdb
Debugger for stepping through code
valgrind
Memory leak checker
strace
Trace system calls
ltrace
Trace library calls
Combine Flags Smartly
✅ Development:
gcc -g -O0 -Wall -o app app.c
✅ Release (optional debug):
gcc -g -O2 -o app app.c
You can also strip symbols before release:
strip app
GCC Toolchain Components
What is a Toolchain?
A toolchain is a set of programming tools used together to build (compile), link, and analyze software — especially in systems programming like C, C++, and embedded development.
Think of it like a production line in a factory: each tool does a specific job to turn your source code into a working executable program.
Basic Steps in a Toolchain:
Preprocessing – handles #include, #define, etc.
Compiling – converts C code into assembly.
Assembling – converts assembly to object code.
Linking – combines object files and libraries into a final executable.
Each of these steps is done by a specific tool.
Common Components in a GCC Toolchain:
Step
Tool
Purpose
Preprocessing
cpp
Processes #include, macros
Compiling
gcc or cc1
Turns C/C++ into assembly code
Assembling
as
Converts .s to .o
Linking
ld
Combines .o files into binary
Debugging
gdb
Debugs your program
Analyzing
objdump, nm, readelf
Analyze and inspect binaries
Why Use a Toolchain?
Automates the process of building software.
Helps you go from source code to executable step by step.
Gives you fine control over what happens at each stage.
Essential in cross-compilation (e.g., compiling code on a PC to run on an embedded board like ARM or ESP32).
Real-World Analogy:
Imagine you’re baking a cake:
Recipe (source code) → You follow steps (preprocess, compile, etc.)
Ingredients → Preprocessed and compiled pieces
Oven → The assembler
Icing and decoration → Linker adds libraries and finishes the executable
At the end, you get a ready-to-eat cake, or in our case, a working program!
In this tutorial, we’ll understand 5 key tools:
as — the assembler
ld — the linker
objdump — the binary disassembler
nm — lists symbols
readelf — reads ELF binary structure
1. as — The Assembler
What it does:
as converts assembly language (.s) into machine code object files (.o).
Concept:
After the compiler generates assembly code from C/C++ (gcc -S), the assembler takes this .s file and converts it into binary instructions that the CPU can understand.
Example:
gcc -S hello.c # Step 1: Get assembly code (hello.s)
as hello.s -o hello.o # Step 2: Assemble to object file
You now have an object file hello.o.
2. ld — The Linker
What it does:
ld combines multiple .o files and libraries into a single executable.
Concept:
It resolves symbol references (like function calls), adds startup code, and organizes the layout of memory sections (text, data, etc.).
Example:
ld -o hello hello.o -lc -dynamic-linker /lib64/ld-linux-x86-64.so.2 -e main
Note: Usually, gcc does this linking automatically. The above is a manual link.
3. objdump — The Disassembler
What it does:
objdump shows you what’s inside an object or binary file. You can view assembly code, symbol tables, and more.
Concept:
Useful for debugging or reverse engineering. You can see what machine code was generated from your C/C++ code.
Example:
objdump -d hello.o
This disassembles the object file — shows actual assembly instructions.
You can also inspect all:
objdump -x hello.o
4. nm — Lists Symbols
What it does:
nm lists symbols (like functions and global variables) from object files or executables.
Concept:
Symbols can be functions, variables, or labels. It shows whether they’re defined, undefined, or global/static.
Example:
nm hello.o
Sample output:
00000000 T main
U printf
T main: symbol main is defined in text (code) section.
U printf: symbol printf is undefined (from libc).
5. readelf — Reads ELF Binaries
What it does:
readelf displays information from ELF (Executable and Linkable Format) files — used by Linux.
Concept:
ELF files are used for executables, object files, shared libs, etc. You can view headers, sections, symbol tables, etc.
gcc -S hello.c # Creates hello.s
as hello.s -o hello.o # Assemble to object file
ld hello.o -o hello -lc -dynamic-linker /lib64/ld-linux-x86-64.so.2 -e main
./hello # Run the program
Inspect:
nm hello.o
objdump -d hello.o
readelf -h hello
You can also Visit other tutorials of Embedded Prep
Real-time questions related to QNX : Real-time systems are crucial in industries that require highly predictable and time-sensitive operations. In embedded systems, especially in applications like automotive, telecommunications, medical devices, and robotics, real-time performance is not just a feature; it is a necessity. Among the many real-time operating systems available, QNX stands out due to its robustness, modularity, and real-time capabilities.
What is QNX?
QNX is a commercial, Unix-like real-time operating system (RTOS) designed primarily for embedded systems. Unlike general-purpose operating systems, QNX is optimized for real-time performance, meaning it can guarantee responses to inputs within strict time constraints. This ensures that time-sensitive tasks are executed without delay, which is crucial for systems that must respond promptly to external stimuli or control systems.
At its core, QNX utilizes a microkernel architecture. The QNX microkernel is a lightweight and efficient core that handles basic operations such as task scheduling, memory management, and inter-process communication (IPC). Everything else, including device drivers, file systems, and network services, runs in user space, isolated from the kernel. This separation enhances system reliability and fault tolerance, ensuring that if one service fails, it does not affect others.
The Significance of Real-Time Performance
In real-time systems, the concept of time is critical. Real-time performance is classified into two categories:
Hard Real-Time: Systems that must meet strict deadlines. Missing a deadline could result in catastrophic failures (e.g., life-support systems in hospitals, automotive safety features).
Soft Real-Time: Systems where deadlines are important but not critical. Missing a deadline does not cause immediate failure, but it may degrade performance or user experience (e.g., streaming services, video conferencing).
QNX is designed to provide hard real-time capabilities, which is essential for applications requiring absolute predictability in time-critical environments. The operating system ensures that tasks are executed within specific time constraints, regardless of system load.
QNX Scheduling and Task Management
One of the most critical aspects of real-time operating systems is how they manage tasks and schedules. In QNX, real-time scheduling is deterministic, meaning it can guarantee that tasks will be executed at the right time without delays.
QNX employs priority-based preemptive scheduling, which ensures that tasks are scheduled according to their urgency. When multiple tasks are ready to execute, the scheduler selects the highest-priority task first. If a higher-priority task becomes ready while a lower-priority task is running, QNX will preempt the current task and execute the higher-priority one immediately. This mechanism helps ensure that critical tasks are always executed on time.
Additionally, round-robin scheduling can be used for tasks with equal priority. In this scheme, each task is given an equal time slice to execute before control is passed to the next task in the queue, ensuring fairness in CPU usage.
Microkernel Architecture and Modularity
The key feature of QNX is its microkernel architecture, which is different from traditional monolithic kernels. In a monolithic kernel, the entire OS runs in supervisor mode, meaning any failure in one part of the OS could affect the entire system. In contrast, QNX’s microkernel only provides essential services, such as managing processes, scheduling, and interrupt handling, while other services like device drivers, file systems, and networking run in user space.
This modular approach offers several benefits:
Fault Isolation: If one service crashes (e.g., a device driver), it does not bring down the entire system. This isolation improves the system’s stability.
Flexibility: Developers can customize QNX by adding or removing modules without affecting the core kernel.
Safety: Critical operations can be isolated, and failures can be contained within their own modules, preventing cascading failures.
The modularity of QNX is beneficial for embedded systems where the operating system needs to be lightweight, flexible, and fault-tolerant.
Real-Time Synchronization in QNX
Synchronization between tasks is essential in a real-time operating system, especially in systems that handle concurrent processes. QNX provides several synchronization mechanisms to manage resource access and ensure tasks execute in a coordinated manner.
Mutexes are used to ensure that only one task can access a shared resource at any given time, preventing race conditions.
Semaphores help coordinate the execution of multiple tasks. Tasks can signal each other to indicate when they are ready to proceed.
Condition Variables allow tasks to wait for specific conditions to be true before they continue executing, offering fine-grained control over task sequencing.
These synchronization tools ensure that tasks in QNX can work together without causing conflicts, delays, or data corruption, which is vital for maintaining real-time performance.
Interrupt Handling in QNX
Interrupts are used to respond to external events in real time, and QNX excels in this area by providing efficient and predictable interrupt handling. The system allows for fast response times to hardware interrupts, ensuring that real-time events are processed immediately.
QNX allows developers to configure Interrupt Service Routines (ISRs), which are specialized functions that respond to interrupts. These routines are designed to execute quickly and efficiently, as interrupt handling must be completed as soon as possible to avoid delays. In addition to regular ISRs, QNX supports threaded ISRs, where interrupt handling can be deferred to a separate thread to process more complex tasks without blocking the real-time system.
Message-Passing and Inter-Process Communication (IPC)
In real-time embedded systems, tasks often need to communicate with one another. QNX uses message-passing as a fundamental mechanism for IPC. This allows different processes (or threads) to exchange data safely without interfering with one another.
QNX offers a range of IPC mechanisms, such as:
Message Queues: Used for sending and receiving messages between tasks.
Mailboxes: Specialized message queues with a fixed number of slots, useful for tasks that need to process a stream of messages.
Signals: A lightweight mechanism to send simple notifications between tasks.
This message-passing system enables real-time tasks to coordinate efficiently and communicate without blocking or delaying critical operations.
Memory Management in QNX
Real-time memory management is another vital aspect of QNX. Memory allocation must be predictable and efficient to avoid delays that could impact real-time performance. QNX employs a real-time memory allocator that ensures quick memory allocation and deallocation while minimizing fragmentation.
One of the key features of QNX is its ability to manage memory across different partitions. Memory resources can be reserved for specific tasks or services, ensuring that each component of the system has the memory it needs to function correctly without interference.
QNX also provides support for shared memory between processes, allowing tasks to share data efficiently while maintaining separation for security and stability.
Fault Tolerance and System Reliability
Reliability and fault tolerance are paramount in real-time systems, and QNX is designed to meet these demands. The system ensures that tasks and processes are isolated from one another, so if one fails, it does not compromise the entire system. Furthermore, watchdog timers can be set up to monitor the system’s health. If a task fails or the system becomes unresponsive, the watchdog timer can trigger a recovery process to restore normal operation.
QNX also supports high-availability and failover mechanisms, which are particularly useful in mission-critical applications like automotive control systems or industrial automation, where system uptime is crucial.
By diving into the details of how QNX works, you can gain the expertise needed to build and optimize embedded real-time applications that meet the strict requirements of modern embedded systems.
💛 Support Embedded Prep
If you find our tutorials helpful and want to support our mission of sharing high-quality embedded system knowledge, you can contribute by buying us a coffee.
Every small contribution helps us keep creating valuable content for learners like you. ☕
Thank you for your support — it truly keeps Embedded Prep growing. 💻✨
1. How does QNX ensure real-time performance?
QNX is a real-time operating system (RTOS) designed to meet strict timing constraints. It ensures real-time performance through several key features:
Microkernel Architecture
The QNX microkernel handles only the most critical tasks: scheduling, interrupt handling, interprocess communication (IPC), and synchronization.
Other services (file system, network, etc.) run in user space, which makes the kernel faster and more predictable.
This ensures high-priority tasks meet their deadlines.
Fast Interprocess Communication (IPC)
QNX’s message-passing IPC is highly optimized and synchronous by default.
It allows fast and deterministic communication between threads and processes.
Timer Accuracy
QNX provides high-resolution timers (microsecond accuracy) using its clock and timer services.
You can schedule precise timeouts, delays, and periodic tasks.
Interrupt Handling
Interrupt Service Routines (ISRs) are short and quick, and they usually defer processing to threads called Interrupt Threads (i-threads), which are real-time threads.
2. What is priority inversion and how does QNX handle it?
What is Priority Inversion?
It occurs when:
A low-priority thread holds a shared resource (e.g., a mutex).
A high-priority thread needs that resource and gets blocked.
Meanwhile, a medium-priority thread preempts the low-priority one, preventing it from releasing the resource.
When a low-priority thread holds a mutex needed by a high-priority thread, QNX temporarily boosts the low-priority thread’s priority to match the high-priority one.
This allows it to finish its work and release the resource quickly, minimizing the delay.
This mechanism is built into QNX’s native synchronization primitives, like pthread_mutex_t with the PRIO_INHERIT protocol.
3. Example: Writing a real-time application in QNX
Let’s write a simple real-time periodic task in QNX that runs every 1 second to simulate sensor reading.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
#include <time.h>
void* sensor_task(void* arg) {
struct timespec interval = {1, 0}; // 1 second
struct timespec next_time;
// Get current time
clock_gettime(CLOCK_REALTIME, &next_time);
while (1) {
// Wait until next scheduled time
next_time.tv_sec += 1;
clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &next_time, NULL);
// Simulate real-time task
printf("Sensor read at time: %ld\n", time(NULL));
}
return NULL;
}
int main() {
pthread_t thread;
pthread_attr_t attr;
struct sched_param param;
// Set thread attributes
pthread_attr_init(&attr);
pthread_attr_setschedpolicy(&attr, SCHED_FIFO); // Real-time policy
param.sched_priority = 50; // High priority (1-255 in QNX)
pthread_attr_setschedparam(&attr, ¶m);
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
// Create real-time thread
pthread_create(&thread, &attr, sensor_task, NULL);
// Join thread (in real use case, main might do other work)
pthread_join(thread, NULL);
return 0;
}
Explanation:
We set the thread scheduling policy to SCHED_FIFO (real-time first-in, first-out).
We use clock_nanosleep with TIMER_ABSTIME to make it wake up precisely every 1 second.
This is deterministic and respects real-time timing.
Example 1: Real-time Message Passing in QNX
QNX’s microkernel uses synchronous message passing (MsgSend, MsgReceive, MsgReply) for interprocess communication (IPC), ensuring predictable and real-time-safe data exchange.
Goal:
A client sends sensor data to a server (e.g., a data logger) using QNX message passing.