Blog

  • Multimeter (Voltage, Current, Continuity) Master Beginner-Friendly Guide 2026

    A multimeter is a must-have tool for electronics beginners. It helps you measure voltage, current, resistance, and continuity, and is essential for diagnosing and troubleshooting circuits.

    1. Understanding the Parts of a Multimeter

    Most digital multimeters (DMMs) include:

    • Display – Shows readings (e.g., voltage, current).
    • Dial (selector knob) – Lets you choose what you want to measure: V (voltage), A (current), Ω (resistance), etc.
    • Ports – Usually three:
      • COM (common) – Black probe goes here.
      • VΩmA – For voltage, resistance, and small current (milliamps).
      • 10A or 20A – For measuring larger currents.

    2. How to Set Up the Multimeter

    1. Insert probes:
      • Black probe → COM
      • Red probe →
        • VΩmA for voltage, resistance, small current.
        • 10A port for high current (more than 200mA).
    2. Turn the dial:
      • Set to V for voltage (DC = ⎓, AC = ~).
      • Set to A for current (DC or AC).
      • Set to Ω or continuity symbol (🔔 or sound wave) for continuity check.

    3. Measuring Voltage

    What is it?

    Voltage is the potential difference between two points (like pressure in a pipe). Measured in volts (V).

    Steps to Measure DC Voltage (e.g., battery, power supply):

    1. Plug red probe into VΩmA port.
    2. Turn the dial to DC Voltage (V⎓).
    3. Touch:
      • Black probe to the negative terminal.
      • Red probe to the positive terminal.
    4. Read the display.

    Example:

    • Measuring a 9V battery should show something close to 9.00V.

    Tips:

    • Always start with a higher voltage range if manual.
    • Multimeters are parallel devices — connect across (not in-line with) the component.

    4. Measuring Current

    What is it?

    Current is the flow of electric charge. Measured in amperes (A).

    Important:

    To measure current, you need to break the circuit and insert the multimeter in series so the current flows through the meter.

    Steps to Measure Current (DC):

    1. Decide current range:
      • Less than 200mA → use VΩmA port.
      • More than 200mA → use 10A port.
    2. Turn the dial to DC Amps (A⎓).
    3. Break the circuit and insert:
      • Red probe toward power supply.
      • Black probe toward load (resistor, LED, etc.).
    4. Power on and read the current on display.

    Example:

    • LED circuit might draw 15-20mA.

    Tips:

    • Never measure current directly across a power source — it will short-circuit!
    • If unsure about current level, start with 10A port to avoid blowing a fuse.

    5. Checking Continuity

    What is it?

    Continuity means there is a complete path for current flow (no break in the wire or circuit). Measured using the beep or resistance.

    Steps to Test Continuity:

    1. Turn the dial to continuity mode (🔔 or sound symbol).
    2. Touch probes together → multimeter should beep (circuit complete).
    3. Test a wire, trace, or fuse:
      • Touch each end with a probe.
      • Beep = good.
      • No beep = open (broken) circuit.

    Example:

    • Testing a jumper wire or PCB track for breaks.

    Tips:

    • Useful for checking if a wire or connection is working without knowing voltage.

    Bonus: Safety & Best Practices

    • 🔌 Always disconnect power before switching modes or ports.
    • ⚠️ Double-check port and dial settings before testing.
    • 🔋 Test your multimeter on a known battery to check if it works.
    • 🚫 Never use the resistance or continuity setting on a live circuit.

    Summary Table

    TaskDial SettingRed Probe PortConnection TypeNotes
    Voltage (DC)V⎓VΩmAParallelMeasure across battery/device
    Current (DC)A⎓VΩmA or 10ASeriesBreak the circuit
    Continuity🔔 or ΩVΩmAAcross pathBeep = good connection

    With Protocol Decoding: UART, SPI, I2C

    A Logic Analyzer is a powerful tool that helps you see digital signals over time and decode communication protocols like UART, SPI, and I2C. It’s perfect for debugging embedded systems and microcontroller projects.

    1. What Is a Logic Analyzer?

    A logic analyzer captures digital signals (0s and 1s) from your circuit and shows them as waveforms on your computer screen. It helps you:

    • Visualize what your microcontroller is sending/receiving.
    • Decode protocols like UART, SPI, and I2C.
    • Debug communication errors or misbehaving devices.

    Popular low-cost logic analyzers work with free software like Saleae Logic or Sigrok / PulseView.

    2. Basic Components of a Logic Analyzer

    • Data Channels – Wires to tap into digital signals (D0, D1, etc.).
    • GND Clip – Ground wire to reference signal level.
    • USB Connector – Connects to your PC.
    • Software – For viewing and decoding signals.

    3. General Setup Instructions

    Step 1: Connect Wires

    • Connect GND of the logic analyzer to GND of your circuit.
    • Connect the analyzer’s D0, D1, etc. to the signal lines you want to capture (e.g., TX, RX, SCL, SDA, MISO, MOSI).

    Step 2: Launch Logic Analyzer Software

    • Use PulseView (open source) or Saleae Logic (if using Saleae device).
    • Select sampling rate (e.g., 2MHz or higher).
    • Select channels to capture.

    Step 3: Capture Data

    • Start the capture.
    • Trigger the communication (e.g., send data from microcontroller).
    • Stop capture and analyze the waveform.

    4. Protocol Decoding

    Now let’s go through UART, SPI, and I2C decoding step by step.


    A. UART (Universal Asynchronous Receiver-Transmitter)

    Overview:

    • 2 wires: TX (transmit), RX (receive)
    • No clock line
    • Asynchronous (timing based on baud rate)

    Wiring:

    • D0 → TX
    • D1 → RX
    • GND → GND

    Setup in Software:

    1. Add a UART decoder in your software.
    2. Assign RX line (e.g., D1).
    3. Set parameters:
      • Baud rate (e.g., 9600, 115200)
      • Data bits: usually 8
      • Stop bits: usually 1
      • Parity: usually none

    What You’ll See:

    • Data bytes sent/received over TX/RX.
    • Decoded as readable ASCII or HEX.

    Use Case: Debug serial communication between Arduino and a sensor.


    B.SPI (Serial Peripheral Interface)

    Overview:

    • 4 wires:
      • MISO – Master In Slave Out
      • MOSI – Master Out Slave In
      • SCK – Clock
      • SS/CS – Slave Select
    • Synchronous communication

    Wiring:

    • D0 → MOSI
    • D1 → MISO
    • D2 → SCK
    • D3 → CS (optional)
    • GND → GND

    Setup in Software:

    1. Add SPI decoder.
    2. Assign:
      • MOSI → D0
      • MISO → D1
      • Clock → D2
      • CS (optional) → D3
    3. Set SPI mode (0 to 3), bit order (MSB first), etc.

    What You’ll See:

    • SPI data packets.
    • Data sent from master to slave (MOSI) and received (MISO).

    Use Case: Debug EEPROM or display modules using SPI.


    C. I2C (Inter-Integrated Circuit)

    Overview:

    • 2 wires:
      • SDA – Data
      • SCL – Clock
    • Each device has an address

    Wiring:

    • D0 → SDA
    • D1 → SCL
    • GND → GND

    Setup in Software:

    1. Add I2C decoder.
    2. Assign:
      • SDA → D0
      • SCL → D1
    3. No other settings needed (auto-detects data/address).

    What You’ll See:

    • Start → Address → Read/Write → Data → ACK/NACK → Stop
    • Decoded addresses and data bytes.

    Use Case: Debug sensors like BMP280, DS1307, OLEDs, etc.


    5. Example: Debug I2C Sensor (BMP280)

    1. Connect logic analyzer:
      • D0 → SDA
      • D1 → SCL
      • GND → GND
    2. Start PulseView capture.
    3. Trigger sensor read from microcontroller.
    4. I2C decoder will show:
      • Address: 0x76
      • Read/Write: W
      • Data: F4, 2F, FC, etc.

    6. Safety Tips & Best Practices

    • ✅ Always connect GND between your circuit and the logic analyzer.
    • ⚠️ Only connect to 3.3V or 5V logic — higher voltages can damage the analyzer.
    • 📉 Choose a sampling rate at least 10x the clock speed (e.g., 2MHz for 100kHz I2C).
    • 🔄 Use short wires to avoid noise and glitches.
    • 🧠 Interpret decoded signals to find bugs in your firmware or mismatched settings.

    Summary Table

    ProtocolWires NeededPins to ConnectSoftware DecoderNotes
    UARTTX, RXD0 → TX, D1 → RXUARTSet baud rate manually
    SPIMISO, MOSI, SCK, CSD0–D3SPISet SPI mode (0-3)
    I2CSDA, SCLD0 → SDA, D1 → SCLI2CAuto-detects most settings

    Guide to Using an Oscilloscope

    Waveform Analysis, Triggering, Noise Detection

    An oscilloscope is a vital tool in electronics for viewing and analyzing electrical waveforms. It allows you to visualize signals in real time, helping you debug circuits, measure signal characteristics, and detect noise.


    1. What Is an Oscilloscope?

    An oscilloscope (also known as a scope) displays voltage signals over time as waveforms, with time on the x-axis and voltage on the y-axis.

    Key Parts:

    • Display: Shows the waveform of the signal.
    • Probes: Used to connect the oscilloscope to your circuit.
    • Controls/Knobs:
      • Time Base: Adjusts the time per division (horizontal scale).
      • Voltage Scale: Adjusts the voltage per division (vertical scale).
      • Trigger: Stabilizes waveforms.
      • Position: Shifts the waveform left/right or up/down.

    2. Setting Up the Oscilloscope

    Step 1: Connect the Probe

    • Plug the oscilloscope probe into the probe input on the oscilloscope.
    • Black clip goes to GND of your circuit.
    • Red probe goes to the signal you’re measuring (e.g., test points, output pins).

    Step 2: Turn on the Oscilloscope

    • Power up your oscilloscope and ensure it’s properly calibrated.

    3. Waveform Analysis: Understanding the Basics

    What is a Waveform?

    A waveform is a graphical representation of an electrical signal’s variation in voltage over time. It allows you to visually inspect:

    • Frequency (how often the wave repeats)
    • Amplitude (the height of the wave, showing signal strength)
    • Shape (sine, square, triangle, etc.)

    What You See on the Screen:

    • The oscilloscope plots the voltage (y-axis) against time (x-axis).
    • The horizontal axis shows time (e.g., 1ms/div), so you can measure signal period (how long one cycle lasts).
    • The vertical axis shows voltage (e.g., 1V/div), letting you see signal amplitude (the peak-to-peak voltage).

    Example:

    If you’re measuring a sine wave signal, it might look like this:

    • Period = time for one complete wave.
    • Frequency = 1 / period.
    • Amplitude = peak-to-peak height of the wave.

    4. Triggering: Stabilizing Your Signal

    What is Triggering?

    Triggering helps you stabilize repetitive waveforms on the oscilloscope display. Without triggering, the waveform may appear as a blurry jumble, making it hard to measure.

    How to Use Triggering:

    1. Trigger Source: Choose which signal to use for triggering (e.g., CH1 for the signal you’re observing).
    2. Trigger Level: Set the voltage level at which the oscilloscope will trigger.
    3. Trigger Mode: Choose between different modes:
      • Edge Triggering: Triggers on a rising or falling edge (default mode).
      • Pulse Width Triggering: Triggers on a signal that is either too long or too short.
      • Video Triggering: Useful for video signals.

    When to Use Triggering:

    • If your waveform is moving around or not stable, adjust the trigger level until it “locks” the waveform in place.
    • Auto mode automatically triggers after some time, but Normal mode waits until a specific signal level is reached.

    5. Noise Detection: Identifying Unwanted Signals

    What is Noise?

    Noise is any unwanted electrical signal that disrupts your intended signal, leading to errors or inaccurate readings.

    How to Detect Noise:

    1. Look for unexpected spikes or irregularities in your waveform.
    2. Measure and observe how noise affects signal stability:
      • Noise can appear as random fluctuations or spikes superimposed on your primary signal.
      • It may cause your waveform to be distorted or noisy.

    Reducing Noise:

    • Use proper grounding: Ensure your circuit and oscilloscope are well-grounded.
    • Shielding: Use shielded cables or enclosures to prevent external interference.
    • Bandwidth Limiting: Use filters or reduce bandwidth to exclude high-frequency noise.
    • Averaging: Some oscilloscopes allow you to average signals, smoothing out noise for better analysis.

    6. Practical Tips for Using an Oscilloscope

    • Start with low time base (e.g., 1ms/div) to get a clear view of the signal.
    • Increase the time base if the signal is too fast to capture (e.g., use 10us/div for high-speed signals).
    • Use the trigger to stabilize repetitive waveforms.
    • Zoom in and out with the time and voltage controls to inspect different aspects of your signal.
    • Use Math Functions: Some oscilloscopes allow you to overlay math functions like adding, subtracting, or averaging waveforms.

    7. Summary of Key Controls

    ControlFunction
    Time BaseControls the time per division (horizontal scale)
    Voltage ScaleControls the voltage per division (vertical scale)
    Trigger SourceSpecifies which signal to trigger on (e.g., CH1)
    Trigger LevelSets the voltage level that triggers the oscilloscope
    PositionShifts the waveform left/right or up/down

    8. Example: Analyzing a Square Wave

    Step-by-Step:

    1. Connect the probe to the square wave generator output.
    2. Set the time base to 1ms/div.
    3. Adjust the trigger level to the middle of the square wave.
    4. Observe the waveform:
      • You’ll see a square wave with sharp transitions (rising and falling edges).
      • The frequency of the square wave is the rate at which the waveform repeats (e.g., 1kHz).
    5. Analyze:
      • Measure the high and low levels of the wave (voltage).
      • Check for symmetry and proper signal integrity.

    9. Conclusion: Your Oscilloscope at a Glance

    An oscilloscope is a powerful tool for waveform analysis, triggering, and noise detection. By understanding the basic controls and techniques:

    • You can measure voltage, frequency, and other properties of your signals.
    • Stabilize waveforms using the trigger function.
    • Detect and mitigate noise issues in your circuits.

    As you practice, you’ll become more comfortable interpreting different waveforms and using the oscilloscope for complex signal analysis.


    Using Power Supply and Lab Tools Safely

    A Beginner’s Guide to Safe Practices in the Electronics Lab

    Working with electronic circuits and equipment can be exciting, but it’s crucial to follow proper safety protocols. Lab tools like power supplies, oscilloscopes, multimeters, and soldering irons are essential for electronics work, but they can also pose risks if used improperly.

    This guide will introduce you to safe practices while using power supplies and lab tools, helping you avoid accidents and ensure a safe, productive environment.


    1. Power Supply Safety

    What Is a Power Supply?

    A power supply provides electrical power to your circuits by converting AC (Alternating Current) from a wall outlet into DC (Direct Current) or supplying a specific type of voltage or current needed by your circuit.

    Basic Safety Precautions:

    a. Know Your Equipment:

    • Before using a power supply, read the manual carefully to understand its voltage, current limits, and other features.
    • Check connections to ensure everything is wired correctly before turning on the power supply.

    b. Set Voltage and Current Limits:

    • Always set the voltage and current limits of the power supply before connecting it to the circuit.
    • If you’re unsure about the voltage requirements of your circuit, start with a low voltage setting to avoid damage.

    c. Use the Right Power Supply:

    • Choose the correct power supply type (e.g., DC power supply for DC circuits) and ensure it matches the voltage and current ratings of your circuit.

    d. Avoid Overloading:

    • Don’t exceed the maximum current limit of the power supply. Overloading can cause the power supply to overheat and potentially damage your circuit or the power supply itself.

    e. Turn Off Power Before Making Changes:

    • Always turn off the power supply and disconnect the leads before changing any circuit connections or making adjustments to your setup.

    f. Use Fuses for Protection:

    • Power supplies often have built-in fuses to protect the circuit from overcurrent. Check if the fuse is properly rated, and always replace fuses with the correct type.

    g. Grounding:

    • Ensure the power supply is properly grounded to avoid electrical shock hazards. A properly grounded setup reduces the risk of short circuits or voltage spikes.

    2. Multimeter Safety

    What Is a Multimeter?

    A multimeter is used to measure voltage, current, and resistance. It’s an essential tool for troubleshooting circuits and verifying correct voltage levels.

    Basic Safety Precautions:

    a. Check Probes and Leads:

    • Inspect the multimeter probes and leads for any damaged insulation before use. Damaged leads can cause short circuits or electrical shocks.

    b. Set the Multimeter Correctly:

    • Ensure you set the multimeter to the correct function (e.g., voltage, current, resistance) and range. For current measurement, ensure the multimeter is in series with the circuit, not in parallel.

    c. Avoid Measuring High Voltage:

    • Always use a multimeter rated for high voltage when measuring circuits with voltages above 30V. High voltage circuits require special care, and the multimeter should be rated for those voltages.

    d. Never Measure Resistance on Live Circuits:

    • Avoid measuring resistance or continuity on circuits that are powered on. Doing so can damage the multimeter and cause electrical hazards.

    e. Use Proper Settings for Current:

    • When measuring current, break the circuit and connect the multimeter in series. Never set the multimeter to current measurement while it is in parallel with a power source.

    3. Soldering Iron Safety

    What Is a Soldering Iron?

    A soldering iron is a tool used to heat and melt solder to connect electronic components to a circuit board. It’s a crucial tool, but the heat it generates makes it dangerous if used carelessly.

    Basic Safety Precautions:

    a. Wear Protective Gear:

    • Wear protective glasses to shield your eyes from solder fumes and any accidental splashes of molten solder.
    • Use heat-resistant gloves if necessary, especially when handling hot components or the soldering iron.

    b. Work in a Well-Ventilated Area:

    • Soldering fumes can be harmful if inhaled. Always use the soldering iron in a well-ventilated area or, ideally, with a fume extractor.

    c. Set the Soldering Iron Down Carefully:

    • Always place the soldering iron on a heat-resistant surface or a soldering stand. Never leave it unattended, as it can cause fires or burns if left on an inappropriate surface.

    d. Turn Off When Not in Use:

    • Always turn off the soldering iron when you’re finished using it, and unplug it when not in use to avoid accidental burns.

    4. Oscilloscope and Other Lab Tools Safety

    📖 What Is an Oscilloscope?

    An oscilloscope is used to visualize and analyze electrical waveforms, such as signals from a microcontroller or sensor. It is a delicate instrument that requires attention to safety.

    Basic Safety Precautions:

    a. Grounding:

    • Ensure that both the oscilloscope and your circuit are properly grounded to avoid electrical shock and ensure accurate measurements.

    b. Use Proper Probes:

    • Use oscilloscope probes that are rated for the voltage levels you are measuring. Some high-voltage circuits require special probes for safety.

    c. Do Not Touch Live Circuitry:

    • Avoid touching live circuitry or components while connected to the oscilloscope to prevent shock and short-circuits.

    d. Handling Capacitors:

    • Discharge capacitors before working with them, as they can hold a charge and give you a shock even when the circuit is powered off.

    e. Avoid Overloading:

    • Like power supplies, oscilloscopes also have limits for voltage and current. Never exceed the voltage ratings of the oscilloscope probe.

    5. General Safety Tips for the Electronics Lab

    • Read manuals before using any tool. Understanding the user manual of each device is crucial to ensure safe usage.
    • Organize your workspace to reduce clutter. A tidy lab helps avoid accidents, makes tools easier to find, and reduces tripping hazards.
    • Use insulated tools when working with electrical circuits to reduce the risk of short circuits and electrical shock.
    • Unplug devices when not in use. Always disconnect equipment from power sources before making adjustments.
    • Wear safety glasses and gloves as a basic measure when working with hot components or sharp objects.

    6. Emergency Preparedness

    Even with all the safety precautions, accidents can happen. Here are a few tips to stay prepared:

    • Know the location of the emergency shut-off switch in the lab or your workspace.
    • Learn basic first aid for electrical burns and shock.
    • Have a fire extinguisher nearby, especially when working with soldering irons and other heat-producing tools.

    Conclusion: Stay Safe and Have Fun

    Using power supplies and lab tools is an essential part of working in electronics, but safety should always come first. By following the guidelines outlined in this article, you’ll minimize risks and create a safer environment for yourself and others. Remember, always respect the tools and circuits you work with, and don’t hesitate to ask for help or advice from more experienced individuals when needed.

    You can also Visit other tutorials of Embedded Prep 

    Special thanks to @mr-raj for contributing to this article on EmbeddedPrep

  • Master TDD Test-Driven Development for Beginners: A Comprehensive Guide 2026

    TDD Test-Driven Development : is a software development process that emphasizes writing tests before writing the actual code. It may sound counterintuitive at first, but it offers numerous benefits, especially for beginners who are looking to improve code quality, reduce bugs, and develop a clear understanding of the desired functionality of their software.

    In this article, we’ll explore what TDD is, its key principles, and how you can implement it step-by-step. By the end of this guide, you will have a clear understanding of TDD and how to apply it effectively to write better software.

    TDD Test-Driven Development

    What is Test-Driven Development (TDD)?

    TDD is a software development approach where developers write a test for a function or feature before writing the code that implements it. The idea is simple:

    1. Write a Test for a small piece of functionality you want to implement.
    2. Run the Test, and watch it fail because the feature isn’t implemented yet.
    3. Write the Code that will make the test pass.
    4. Refactor the Code to improve its structure or efficiency, ensuring that the test still passes.

    This cycle is often referred to as the Red-Green-Refactor cycle:

    • Red: Write a failing test (since the code to pass it doesn’t exist yet).
    • Green: Write the code to make the test pass.
    • Refactor: Clean up and improve the code while ensuring the test still passes.

    Why Use TDD?

    TDD might feel like an additional step in the process, but it has several advantages that make it worthwhile:

    1. Improved Code Quality: Writing tests first forces you to think through the problem and its requirements. This leads to cleaner, well-structured code that is easier to understand.
    2. Reduced Debugging Time: Since you’re constantly testing your code in small increments, bugs are easier to identify and fix early in the process.
    3. Confidence in Refactoring: The tests act as a safety net, allowing you to refactor code without worrying about breaking functionality.
    4. Clear Specifications: Tests serve as documentation, defining how your code is expected to behave.
    5. Easier Maintenance: Over time, having a comprehensive test suite makes it easier to maintain and extend the software with minimal risk of introducing new bugs.

    Key Principles of TDD

    Before diving into TDD, it’s helpful to understand the key principles that guide the process.

    1. Small Increments: TDD is about writing small tests for small pieces of functionality, and then incrementally improving the code. This approach avoids the complexity of large, untested codebases.
    2. Focus on Behavior, Not Implementation: In TDD, the focus is on how the system should behave, not how it should be implemented. This keeps the development process more modular and adaptable.
    3. Test Every Functionality: TDD advocates for writing tests for all parts of the code, including edge cases and error conditions. This ensures that the software works in all scenarios.
    4. Frequent Refactoring: Refactoring is a key aspect of TDD. Once tests are passing, you should clean up the code, make it more efficient, and improve its structure without changing its functionality.

    The TDD Cycle: A Step-by-Step Guide

    Let’s break down the Red-Green-Refactor cycle with an example. We’ll implement a simple function that adds two numbers.

    Step 1: Write a Failing Test (Red)

    The first step in TDD is to write a test. At this point, we don’t have the function to add numbers yet, so the test will fail when we run it.

    Let’s write a test in Python using a popular testing framework, unittest:

    import unittest
    
    def add(a, b):
        pass  # Function not implemented yet
    
    class TestAddFunction(unittest.TestCase):
        def test_add(self):
            self.assertEqual(add(2, 3), 5)  # Test that 2 + 3 equals 5
    
    if __name__ == '__main__':
        unittest.main()
    

    Now, when we run this test, it will fail because we haven’t implemented the add function yet.

    Step 2: Write the Code to Pass the Test (Green)

    Next, we implement just enough code to make the test pass. In this case, we’ll implement the add function:

    def add(a, b):
        return a + b
    

    Now, when we run the test again, it should pass because the function correctly adds two numbers.

    Step 3: Refactor the Code (Refactor)

    At this stage, the code works, but we can always improve it. However, it’s important not to refactor the code too much until we have a suite of tests in place, so we don’t break anything accidentally. In this case, there’s not much to refactor, but let’s assume we want to optimize the function further.

    For example, if we had added additional functionality, like input validation, we might refactor the code to ensure readability and efficiency.

    After refactoring, we rerun the tests to ensure everything still works as expected.

    TDD in Practice: Example with Multiple Tests

    To further understand TDD, let’s add a few more test cases for edge cases.

    import unittest
    
    def add(a, b):
        return a + b
    
    class TestAddFunction(unittest.TestCase):
        def test_add(self):
            self.assertEqual(add(2, 3), 5)  # Test with normal numbers
    
        def test_add_negative(self):
            self.assertEqual(add(-2, -3), -5)  # Test with negative numbers
    
        def test_add_zero(self):
            self.assertEqual(add(0, 0), 0)  # Test with zero
    
        def test_add_large_numbers(self):
            self.assertEqual(add(1000000, 2000000), 3000000)  # Test with large numbers
    
    if __name__ == '__main__':
        unittest.main()
    

    Here, we have four different test cases. The next steps would be:

    1. Write the function to pass the tests (already done).
    2. Refactor the code if needed.
    3. Run the tests regularly as we implement more features.

    Common TDD Pitfalls to Avoid

    1. Skipping Tests for Small Features: Even if a feature seems small, it’s important to write tests for it. Small features often have edge cases that are easy to overlook.
    2. Not Refactoring Enough: Refactoring is a crucial part of TDD. It keeps your codebase clean, modular, and maintainable.
    3. Focusing Too Much on Testing: While writing tests is important, it’s essential to not overcomplicate the tests. Write tests that are meaningful and test the behavior of the software, not the implementation details.
    4. Skipping Tests for Legacy Code: When working with legacy code, it might be tempting to avoid writing tests for the existing code. However, introducing tests into legacy code helps with long-term stability and maintenance.

    Conclusion

    Test-Driven Development is a powerful technique for creating high-quality software. It encourages writing tests first, which ensures that your code meets the desired specifications from the start. The Red-Green-Refactor cycle helps you write small, focused tests that make it easier to catch bugs early and maintain the code over time.

    By practicing TDD, you can improve your coding skills, produce cleaner code, and build software that is more reliable and easier to maintain. As a beginner, adopting TDD may take some time, but once you get into the rhythm, you’ll find it to be a highly effective and rewarding approach to software development.

    TDD Interview Questions

    1. What is Test-Driven Development (TDD), and how does it differ from traditional testing approaches?
    2. Explain the “Red-Green-Refactor” cycle in TDD.
    3. What are the advantages of using TDD in embedded software development?
    4. What is the role of unit tests in TDD, and why are they important?
    5. How do you decide which tests to write first in TDD?
    6. What are some challenges you may face when implementing TDD in an embedded system environment?
    7. What tools and frameworks are commonly used for TDD in embedded systems?
    8. What is the purpose of mocks and stubs in TDD, and how do you use them effectively?
    9. What are some potential downsides or limitations of TDD?
    10. Can you describe a situation where TDD might not be the best approach?

    Writing Tests in TDD

    1. How do you write effective and meaningful test cases in TDD?
    2. What is the difference between functional and non-functional tests in the context of TDD?
    3. How do you handle edge cases and error conditions in your tests when following TDD?
    4. What do you mean by the “first failing test” in TDD, and how do you use it to guide your development?
    5. How do you ensure that your tests are comprehensive enough to cover all requirements while practicing TDD?

    TDD Best Practices

    1. What are some best practices for writing maintainable tests in TDD?
    2. How do you ensure that your unit tests remain fast and reliable when applying TDD?
    3. How do you avoid writing tests that are too coupled to the implementation details?
    4. What is the importance of code coverage in TDD, and how do you measure it?
    5. How do you handle dependencies when writing unit tests in TDD?

    Advanced TDD Topics

    1. How do you apply TDD when working with hardware components in embedded systems?
    2. What is the role of integration testing and system testing in the TDD process?
    3. How do you handle tests for asynchronous code or code with side effects in TDD?
    4. Can you apply TDD in multi-threaded or real-time embedded systems? If so, how?
    5. How do you handle performance and memory-related tests in TDD for embedded systems?

    TDD in Continuous Integration (CI) / Continuous Deployment (CD)

    1. How does TDD integrate with Continuous Integration/Continuous Deployment pipelines?
    2. What challenges might arise when running automated tests as part of a CI/CD pipeline in embedded systems?
    3. How do you ensure that TDD practices improve the overall quality and speed of your development process?

    You can also Visit other tutorials of Embedded Prep 

    Special thanks to @mr-raj for contributing to this article on EmbeddedPrep

  • Master Unit Testing Frameworks: Google Test 2026

    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:

    1. Clone the Google Test repository from GitHub: git clone https://github.com/google/googletest.git cd googletest
    2. 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.

    1. Include the header file in your test source code: #include <gtest/gtest.h>
    2. 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;
    }
    
    
    1. 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();
    }
    
    1. 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

    1. What is Google Test (gtest)?
      • Explain what Google Test is and its key features.
    2. 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.
    3. 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.
    4. What is the difference between ASSERT_* and EXPECT_* in Google Test?
      • Explain when to use ASSERT_* and EXPECT_* assertions.
    5. What is a test fixture in Google Test?
      • Describe what a test fixture is and provide an example use case.
    6. 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?
    7. What is the role of the main function in Google Test?
      • What does the main function do in a Google Test framework?
    8. 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.
    9. 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.
    10. How do you handle exceptions in Google Test?
      • Can Google Test catch and test exceptions thrown by your code?

    Intermediate Questions

    1. 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?
    2. 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.
    3. 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.
    4. How do you perform performance testing using Google Test?
      • Is it possible to use Google Test for performance benchmarking, and if so, how?
    5. 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.
    6. 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.
    7. 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.
    8. How would you debug a failing test in Google Test?
      • What strategies do you use to diagnose and fix failing tests in Google Test?
    9. 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.
    10. How can you use Google Test to test multithreaded code?
      • Explain how to write tests for code involving threads and concurrency.

    Advanced Questions

    1. 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?
    2. 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.
    3. Can you explain Google Test’s DeathTest feature?
      • What is a “Death Test,” and when would you use it in Google Test?
    4. 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.
    5. How would you create custom assertions in Google Test?
      • Describe how to create custom assertions to extend Google Test’s built-in functionality.
    6. 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.
    7. 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.
    8. Explain the role of Google Test in TDD (Test-Driven Development).
      • How does Google Test support the Test-Driven Development approach in embedded software?
    9. 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.
    10. 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 

    Special thanks to @mr-raj for contributing to this article on EmbeddedPrep

  • Master Static Code Analysis Using Cppcheck | Beginner-Friendly Guide 2026

    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?

    1. Parsing the Code: The tool reads and interprets the code’s structure, including functions, loops, variables, and conditionals.
    2. Rule Matching: The tool compares the code against a set of predefined rules (best practices, coding standards, and error patterns).
    3. Report Generation: Once the analysis is complete, the tool generates a report highlighting any issues, warnings, or suggestions.
    4. 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:

    Step 1: Install Cppcheck
    1. Linux: sudo apt install cppcheck
    2. Windows:
    3. macOS: brew install cppcheck
    Step 2: Example Code

    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
    1. Null Pointer Dereference: Instead of dereferencing the null pointer, you can add a check before dereferencing: if (ptr != nullptr) { *ptr = 42; }
    2. 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:

    1. What is static code analysis, and how does it differ from dynamic code analysis?
    2. What are the benefits of using static code analysis tools in the software development lifecycle?
    3. Can you explain the basic functioning of Cppcheck?
    4. How does Cppcheck identify potential bugs or issues in the code?
    5. What types of issues can Cppcheck detect in C/C++ code?
    6. How do static analysis tools help in improving code quality?
    7. What are the advantages of using Cppcheck over other static analysis tools?
    8. How would you integrate Cppcheck into a CI/CD pipeline?
    9. Can you give an example of a scenario where static code analysis is crucial in embedded systems development?

    Intermediate Questions:

    1. What are the most common types of warnings or errors that Cppcheck detects?
    2. Explain the difference between false positives and false negatives in static code analysis tools.
    3. What are the different types of checks that Cppcheck performs (e.g., memory leaks, null pointer dereferencing)?
    4. How would you configure Cppcheck to check for specific coding standards or rules in your project?
    5. What are some limitations of Cppcheck in comparison to other static analysis tools like Clang Static Analyzer or SonarQube?
    6. Explain the concept of “code smells” and how Cppcheck helps in identifying them.
    7. Can you modify the rule set or configure custom rules in Cppcheck? How would you do that?
    8. What are some common coding mistakes or bad practices that Cppcheck helps to identify?
    9. How do you interpret and resolve the warnings or errors provided by Cppcheck?

    Advanced Questions:

    1. How would you optimize the performance of Cppcheck when analyzing large codebases?
    2. Can Cppcheck be used for code quality checks in both embedded systems and applications? Explain why or why not.
    3. What are the various options available in Cppcheck to customize the output (e.g., format, severity levels, etc.)?
    4. Explain how Cppcheck integrates with version control systems like Git to perform automated code analysis during code commits or pull requests.
    5. Can you use Cppcheck with other programming languages apart from C/C++? If yes, how?
    6. What are the main performance-related issues that Cppcheck detects in embedded systems code?
    7. How would you address memory leak warnings in embedded systems detected by Cppcheck?
    8. Have you ever encountered a situation where Cppcheck generated false positives? How did you handle it?
    9. How does Cppcheck deal with external libraries or third-party dependencies in the codebase?
    10. What are the trade-offs between using static analysis and other testing methods (like unit testing or dynamic analysis) for embedded systems?
    11. 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:

    1. 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?
    2. You have a legacy codebase with several issues. How would you approach using Cppcheck to clean up the code and improve its maintainability?
    3. While using Cppcheck, you find multiple warnings about unused variables in your code. How would you resolve this issue?
    4. During static code analysis with Cppcheck, you notice a warning related to an uninitialized pointer. How would you address this problem?
    5. How would you configure Cppcheck to generate reports for a large-scale project while focusing on high-priority issues?
    6. 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:

    1. Explain how Cppcheck handles complex C++ features like templates, inheritance, and polymorphism.
    2. How do you interpret Cppcheck’s output when it reports a “nullPointerDereference” warning?
    3. What are the possible reasons for Cppcheck to miss certain errors or issues?
    4. What are the command-line options you can use with Cppcheck to improve the efficiency of the analysis?
    5. Can you integrate Cppcheck into a development IDE like Visual Studio or Eclipse? If so, how?
    6. What does the Cppcheck warning unusedFunction mean, and how can you handle it?
    7. Can you use Cppcheck to perform static analysis on code that includes both C and C++ files?
    8. What is the purpose of the --enable=all option in Cppcheck, and when would you use it?
    9. 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 

    Special thanks to @mr-raj for contributing to this article on EmbeddedPrep

  • Master Integration Testing Basics | Beginner-Friendly Guide (2026)

    Integration testing is a crucial step in the software development process, ensuring that different modules or components of a system work together as expected. Unlike unit testing, which tests individual components in isolation, integration testing focuses on the interaction between these components.

    In this tutorial, we’ll break down integration testing for beginners, explaining its importance, types, methods, and providing examples to help you get started.

    What is Integration Testing?

    Integration testing is a type of software testing that focuses on verifying how different modules or components of a system interact and integrate with each other. It is typically conducted after unit tests (which test individual components) and before system testing (which tests the complete system).

    The goal is to identify issues that might arise when different parts of a system interact, such as problems with data flow, incorrect interfaces, or communication failures between modules.

    Why is Integration Testing Important?

    Integration testing helps detect issues such as:

    • Data mismatches: Problems that occur when data is passed between modules, like incorrect formatting or missing fields.
    • Incorrect interfaces: Bugs that appear when two modules do not interact correctly, due to incorrect assumptions about the interface or behavior.
    • Communication issues: Problems in communication between software components, especially in distributed systems.

    Without integration testing, you might miss bugs that only appear when different components of a system work together.

    Types of Integration Testing

    1. Big Bang Integration Testing

    In Big Bang testing, all the modules or components are integrated together at once, and the system is tested as a whole. This approach can be risky because it might be difficult to isolate the root cause of any issues.

    Pros: Simple to set up initially.
    Cons: Hard to debug, and identifying the source of a problem is challenging.

    2. Incremental Integration Testing

    In incremental integration testing, modules are integrated one by one, and testing is done after each integration step. This allows you to catch issues early, making the debugging process easier.

    Types of Incremental Integration Testing:

    • Top-Down Integration: Testing starts with the top-level modules (or components) and works down to the lower-level modules.
      • Advantages: You can test high-level functionality early.
      • Disadvantages: Lower-level modules may be stubbed out (replaced with placeholders) initially.
    • Bottom-Up Integration: Testing starts with the lower-level modules and works upwards.
      • Advantages: Lower-level modules are fully tested first.
      • Disadvantages: Higher-level modules may require stubs for testing.
    • Sandwich Integration: A mix of top-down and bottom-up approaches, where both high-level and low-level modules are tested simultaneously, working towards each other.

    Integration Testing Process

    Here’s a step-by-step guide to the integration testing process:

    Step 1: Understand the Modules and Their Interfaces

    Before starting integration testing, you must understand how the components/modules interact with each other. This includes knowing:

    • Input and Output: What data each module takes in and produces.
    • APIs: How the modules communicate (e.g., function calls, REST APIs, database queries).
    • Dependencies: Any external systems or modules that your component depends on.

    Step 2: Create Integration Test Cases

    Integration test cases are based on the interactions between components. These cases should test the following:

    • Data flow: Does data flow correctly between components?
    • Error handling: Does the system handle errors from other components gracefully?
    • Performance: Do the modules work efficiently when integrated?

    Step 3: Set Up the Environment

    Ensure that the necessary dependencies (e.g., databases, APIs, external services) are available. For example, if you’re testing a web application, you might need to set up a server and a database.

    Step 4: Execute the Tests

    Run the integration tests and monitor how the modules interact. Log all the test results for analysis.

    Step 5: Analyze Results and Debug

    Once tests are executed, check the logs and test results to identify any issues. If there are failures, isolate the module causing the issue and fix it.

    Step 6: Iterate and Improve

    Integration testing is an iterative process. After fixing identified issues, rerun tests to confirm that the issues are resolved and no new problems have been introduced.

    Common Challenges in Integration Testing

    • Complexity: Large systems with many components can be challenging to test because of the number of interactions between modules.
    • Dependency Issues: Some modules may depend on external services or systems, making it hard to test in isolation.
    • Timing and Synchronization: When testing distributed systems or real-time applications, ensuring proper synchronization between components can be difficult.

    Best Practices for Integration Testing

    1. Test Early and Often

    Perform integration testing early in the development cycle and frequently as new components are added or modified. This helps to catch issues before they become too complex or expensive to fix.

    2. Automate Tests

    Automating integration tests can speed up the process, especially for large systems. Use tools like Jenkins, Travis CI, or GitLab CI for continuous integration testing.

    3. Isolate Components Where Possible

    If possible, isolate components using mock services or stubs for external dependencies. This helps you focus on testing the integration of the modules without worrying about external factors.

    4. Use Version Control

    Ensure that all code is properly version-controlled. This makes it easier to revert to previous versions if integration issues arise.

    5. Test for Edge Cases

    Don’t just test the happy path (the ideal case). Test how the system behaves under unusual or error-prone conditions, such as invalid data, network failures, or unexpected input.

    Tools for Integration Testing

    Here are some popular tools to help with integration testing:

    • JUnit (Java): A widely used framework for writing and running tests in Java.
    • Selenium: A tool for automating web browsers, often used for integration testing of web applications.
    • Postman: A tool for testing APIs, particularly useful for integration testing involving web services.
    • SoapUI: A tool for testing SOAP and REST APIs, useful for integration testing of web services.
    • Jenkins: A popular tool for automating integration testing in continuous integration pipelines.

    Conclusion

    Integration testing is a vital step in ensuring the overall functionality and reliability of a system. By focusing on how individual components interact, integration testing helps identify and resolve issues before they impact the end user.

    Following best practices, using the right tools, and applying systematic approaches to integration testing will ensure your software performs as expected in real-world environments.

    Start by understanding how your system’s components interact, write meaningful test cases, and iteratively test your system to catch any integration-related bugs.

    FAQ for Integration Testing Basics

    1. What is Integration Testing?

    Integration testing is the phase in software testing where individual components or systems are combined and tested as a group. The goal is to ensure that the modules or components work together as expected.

    2. Why is Integration Testing Important?

    Integration testing is crucial because it identifies issues in the interaction between integrated units. While unit testing verifies individual components, integration testing ensures that the combined modules function correctly as a whole.

    3. What Are the Types of Integration Testing?

    There are several approaches to integration testing:

    • Big Bang Integration Testing: All components are integrated at once and tested together.
    • Incremental Integration Testing: Components are integrated one by one and tested in small parts. It can be:
      • Top-down: Testing starts with the top-level modules and progressively moves downward.
      • Bottom-up: Testing starts with the lower-level modules and moves upward.
      • Sandwich/Hybrid: A combination of top-down and bottom-up.

    4. What Are the Key Objectives of Integration Testing?

    • To verify data flow between components.
    • To identify problems that occur when multiple components interact.
    • To detect interface issues.
    • To ensure the overall system performs as expected.

    5. How Is Integration Testing Different from Unit Testing?

    • Unit Testing focuses on testing individual units or components of the system in isolation.
    • Integration Testing checks the interaction between different units or components after they’ve been integrated.

    6. What Are the Common Challenges in Integration Testing?

    • Complexity: With many components to test, managing and tracking tests becomes difficult.
    • Environment Setup: Setting up the integrated system or environment may be time-consuming.
    • Data Issues: Ensuring accurate data flows between components can be tricky.
    • Interface Compatibility: Verifying that the interfaces between components work correctly.

    7. What Tools Are Used for Integration Testing?

    Common tools for integration testing include:

    • JUnit (for Java)
    • TestNG (for Java)
    • Postman (for API testing)
    • SoapUI (for SOAP and REST APIs)
    • Selenium (for web application testing)
    • Mockito (for mocking objects in unit and integration testing)

    8. What Is a Test Harness in Integration Testing?

    A test harness is a set of tools, scripts, and procedures used to automate the testing of an integrated system. It helps in setting up the environment, invoking tests, and validating results.

    9. How Do You Write Test Cases for Integration Testing?

    Integration test cases should focus on:

    • Data Flow Validation: Ensuring data is passed correctly between components.
    • Functional Testing: Ensuring that combined components work together to perform the intended functionality.
    • Error Handling: Ensuring the system handles errors correctly when interactions between modules fail.

    10. What is Stubbing and Mocking in Integration Testing?

    • Stubbing: Replacing a module or function with a controlled response to isolate the components under test.
    • Mocking: Similar to stubbing, but involves creating a fake object to simulate the behavior of a component or service.

    11. How Do You Handle Dependencies in Integration Testing?

    During integration testing, external dependencies (like databases or services) should be either mocked or simulated if they are unavailable. This ensures that the integration test remains focused on the functionality of the integrated components rather than the external services.

    12. When Should Integration Testing Be Performed?

    Integration testing should be performed after individual components have passed unit testing and before system testing or user acceptance testing (UAT).

    13. What Are Common Integration Testing Techniques?

    • Interface Testing: Validating that communication between integrated units follows the expected protocol.
    • Data Integrity Testing: Ensuring that data flows correctly between components and is transformed as expected.
    • Error Path Testing: Verifying that errors are handled correctly when a component fails or behaves unexpectedly.

    14. How Do You Measure the Effectiveness of Integration Testing?

    Effectiveness can be measured by:

    • The number of defects found during integration.
    • The level of coverage (i.e., how many integration scenarios were tested).
    • The time taken to resolve defects found during integration testing.

    15. What Is the Role of Continuous Integration (CI) in Integration Testing?

    In Continuous Integration (CI), integration tests are automatically executed whenever a new piece of code is pushed to the repository. This helps in catching issues early and ensures that newly integrated components don’t break the system.

    You can also Visit other tutorials of Embedded Prep 

    Special thanks to @mr-raj for contributing to this article on EmbeddedPrep

  • Master System Testing | Beginner-Friendly Guide 2026

    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:

    1. Functional Testing: Validates that the system behaves according to the functional specifications.
      • Example: Testing whether the user login feature works as expected.
    2. 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.
    3. Security Testing: Ensures that the system is secure from threats and unauthorized access.
      • Example: Testing for SQL injection vulnerabilities in a web application.
    4. 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.
    5. 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.
    6. 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.
    7. 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

    1. Understand the System Requirements
      • Review the functional and non-functional requirements of the system. These will be the basis for your test cases.
    2. Develop Test Plan
      • A test plan outlines the scope, objectives, and approach for system testing. It also includes the resources, schedule, and risk assessment.
    3. 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
    4. Set Up the Test Environment
      • Prepare the hardware, software, and network environment for testing. This may involve setting up servers, databases, and testing tools.
    5. Execute Test Cases
      • Run the test cases and observe the system’s behavior. Record the actual results and compare them with the expected results.
    6. 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.
    7. Retest
      • Once defects are fixed, retest the system to ensure the issues have been resolved and that no new issues have been introduced.
    8. 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:
      1. Open the login page.
      2. Enter a valid username and password.
      3. 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

    1. Start Early: Begin system testing early in the software development life cycle (SDLC) to detect defects as soon as possible.
    2. Test in Different Environments: Make sure to test the system in various environments to ensure compatibility.
    3. Automate Repetitive Tests: Use test automation tools to run repetitive tests, especially for regression testing.
    4. Maintain Clear Documentation: Keep detailed logs of test cases, test results, defects, and fixes.
    5. Involve Different Stakeholders: Include various team members such as developers, QA testers, and business analysts to get different perspectives on the system.
    6. 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 

    Special thanks to @mr-raj for contributing to this article on EmbeddedPrep

  • MISRA C MISRA C++ Guidelines | Master Beginner-Friendly Tutorial 2026

    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++

    FeatureMISRA-CMISRA-C++
    Language FocusC (especially C90, C99, C11)C++ (mainly C++03, some C++11)
    Use CaseWidely used in embedded CLess common, but used in modern automotive software (e.g., ADAS)
    Latest VersionMISRA 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)
    • Rule – Detailed code-specific rule (e.g., avoid goto)

    Common MISRA C MISRA C++ Rules (Simplified Examples)

    RuleDescriptionExample
    Rule 1.1Only standard headers must be used#include <stdio.h>#include "myheader.h" (if non-standard)
    Rule 8.7Functions should have internal linkage if not used outside the filestatic void helper()
    Rule 11.4Avoid casting between pointers to different object typesint *x = (int*)ptr;
    Rule 17.7Do not use gotogoto error;
    Rule 21.1Use of standard library functions should be limitedsystem("rm -rf /")

    Common MISRA-C++ Rules (Simplified Examples)

    RuleDescriptionExample
    Rule 0-1-1Avoid compiler-specific extensions__attribute__((packed))
    Rule 5-0-15Do not use reinterpret_castreinterpret_cast<int*>(ptr)
    Rule 6-4-1Do not use multiple inheritanceclass A: public B, public C
    Rule 7-1-1Avoid using new and deletePrefer static memory or smart pointers

    How to Start Using MISRA in Your Project

    1. Read the Guidelines
      • Official documents are paid but summaries are available online
    2. Use a Static Code Analyzer
      Tools like:
      • Parasoft
      • PC-lint / FlexeLint
      • Coverity
      • Cppcheck (partial support)
    3. Write Clean and Portable Code
      Follow:
      • No goto
      • No dangerous casts
      • No dynamic memory unless necessary
      • Always initialize variables
    4. 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
    }
    

    Resources to Learn More

    • MISRA Official Site
    • Static analysis tools with MISRA support
    • Open-source GitHub repos demonstrating MISRA compliance
    • Automotive safety books (ISO 26262 + MISRA)

    When to Use MISRA-C and MISRA-C++

    Use MISRA-C When:

    1. Your project is written in C
      • Example: Low-level firmware, device drivers, or bare-metal embedded systems.
    2. You’re working with microcontrollers with limited memory and no operating system.
    3. Safety and reliability are critical (automotive ECUs, medical devices, industrial sensors).
    4. You want to avoid risky or undefined behaviors in C (e.g., pointer misuse, buffer overflows).
    5. 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:

    1. Your project uses C++ instead of C.
    2. You’re building complex embedded applications using object-oriented design (e.g., control systems, infotainment systems).
    3. You want to leverage C++ features (e.g., classes, inheritance, templates) but still need safety and predictability.
    4. Your team has C++ experience and your toolchain supports it.
    5. 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

    TermMeaning
    MISRACoding rules for safety-critical software
    MandatoryMust follow
    RequiredShould follow (document if not)
    Static ToolTool to check MISRA violations

    Beginner-Level Questions

    1. What is MISRA? Why is it important?
    2. What are the goals of MISRA-C / MISRA-C++?
    3. What is the difference between MISRA-C and MISRA-C++?
    4. What do “Mandatory”, “Required”, and “Advisory” rules mean?
    5. What is undefined behavior in C, and how does MISRA help avoid it?
    6. Why does MISRA disallow the use of goto?
    7. Why should dynamic memory (malloc, new) be avoided in embedded systems according to MISRA?
    8. Why does MISRA suggest using static for functions not used outside the file?
    9. Why is type casting between incompatible types discouraged in MISRA?
    10. Can you name a few standard C functions that MISRA recommends avoiding?

    Intermediate-Level Questions

    1. Explain the difference between a directive and a rule in MISRA.
    2. How does MISRA ensure portability across compilers and architectures?
    3. What are some limitations or criticisms of MISRA rules?
    4. How do you apply MISRA rules in a real project?
    5. What tools have you used to enforce MISRA compliance?
    6. What is a deviation? How is it handled in MISRA compliance?
    7. Explain Rule 11.4 (Avoid conversion between pointers to different object types).
    8. Why does MISRA-C++ restrict the use of multiple inheritance and virtual functions?
    9. How would you fix a violation of Rule 8.7 (Functions with internal linkage)?
    10. How do you handle third-party code that violates MISRA guidelines?

    Advanced-Level Questions

    1. How would you implement a MISRA compliance process in an existing codebase?
    2. What challenges do you face while using static analysis tools for MISRA?
    3. How do MISRA-C and ISO 26262 relate in terms of functional safety?
    4. What are the implications of using reinterpret_cast in C++ for safety-critical systems?
    5. Explain how MISRA helps in enabling formal verification or model-based design.
    6. Can you give an example of a rule violation that caused a bug or issue in production?
    7. How do you justify a deviation with traceability to requirements?
    8. How do you balance performance optimization and MISRA compliance?
    9. What is the difference between MISRA C:2004 and MISRA C:2012?
    10. 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:

    1. Understanding basic C/C++ syntax and behavior
    2. Reading MISRA rule categories (e.g., syntax, type safety, portability)
    3. Studying real-world code examples
    4. 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 

    Special thanks to @mr-raj for contributing to this article on EmbeddedPrep

  • Master Code Coverage Tools: A Beginner-Friendly, In-Depth Tutorial (2026)

    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

    TypeDescriptionExample
    Line Coverage% of lines executed90 out of 100 lines = 90%
    Function Coverage% of functions called8 of 10 functions = 80%
    Branch Coverage% of if/else branches executedBoth if and else parts
    Condition Coverage% of boolean sub-expressions testedif (A && B) tests A and B
    Path Coverage% of possible execution paths testedAll loops + conditions

    Popular Code Coverage Tools

    ToolLanguagePlatformUsage
    gcovC/C++LinuxBuilt-in with GCC
    lcovC/C++LinuxVisual frontend for gcov
    gcovrC/C++Cross-platformOutputs XML/HTML
    JaCoCoJavaCross-platformJava code coverage
    Coverage.pyPythonCross-platformSimple and powerful
    IstanbulJavaScriptNode.jsFor JS/TS projects

    Tutorial: Using gcov and lcov (For C/C++ Beginners)

    Let’s walk through a simple example.

    Step 1: Setup a Simple C Program

    // file: example.c
    #include <stdio.h>
    
    void test_function(int x) {
        if (x > 0)
            printf("Positive\n");
        else
            printf("Non-positive\n");
    }
    
    int main() {
        test_function(5);
        return 0;
    }
    

    Step 2: Compile with Coverage Flags

    gcc -fprofile-arcs -ftest-coverage example.c -o example
    

    Explanation:

    • -fprofile-arcs – Instruments code to generate branch info.
    • -ftest-coverage – Generates .gcno file for test info.

    Step 3: Run the Program

    ./example
    

    What happens?

    • It runs the code.
    • Creates .gcda file (stores execution count).

    Step 4: Run gcov to See Coverage

    gcov example.c
    

    You’ll get output like:

    File 'example.c'
    Lines executed:80.00%
    example.c.gcov created
    

    Look into example.c.gcov – it has line-by-line coverage.

    Step 5 (Optional): Visualize with lcov and genhtml

    lcov --capture --directory . --output-file coverage.info
    genhtml coverage.info --output-directory out
    xdg-open out/index.html
    

    Coverage in CI/CD (Advanced Tip)

    Use tools like gcovr, lcov, or Codecov in GitHub Actions or Jenkins pipelines to:

    • Fail builds if coverage is low
    • Track coverage over time
    • Generate HTML badges for repos

    Understanding Output Files

    FilePurpose
    .gcnoGenerated during compilation (static info)
    .gcdaGenerated after running executable (runtime info)
    .gcovResult from combining both (readable format)

    Code Coverage vs Testing

    ConceptPurpose
    Unit TestsCheck if individual components work
    Code CoverageChecks if tests touch all code
    🧠 Best Practice: Use both together!

    Common Mistakes to Avoid

    1. Thinking 100% coverage means no bugs – Not true! Code may run but still produce wrong output.
    2. Forgetting to run all test cases before measuring – Always execute full test suite first.
    3. Not cleaning old coverage data – Use gcovr -r . --delete or clean .gcda files before retesting.

    Bonus: gcovr for Easy Automation

    pip install gcovr
    gcovr --html --html-details -o coverage.html
    

    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:

    1. Write a simple C program.
    2. Compile it with code coverage flags.
    3. Run it to generate coverage data.
    4. Use gcov to view the coverage.
    5. 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;
    }
    

    Step 2: Compile with Coverage Flags

    gcc -fprofile-arcs -ftest-coverage math_utils.c -o math_utils
    

    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.

    Bonus: Generate Visual Report with lcov + genhtml

    Step 1: Install lcov

    sudo apt install lcov
    

    Step 2: Collect Coverage Info

    lcov --capture --directory . --output-file coverage.info
    

    Step 3: Generate HTML Report

    genhtml coverage.info --output-directory out
    

    Step 4: Open Report

    xdg-open out/index.html
    

    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 

    Special thanks to @mr-raj for contributing to this article on EmbeddedPrep

  • Embedded Boot Process Overview | Master Beginner-Friendly Guide 2026

    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. The CPU runs from ROM and looks for a bootloader.
    2. U-Boot is loaded into RAM.
    3. U-Boot initializes hardware (memory, CPU, storage, etc.).
    4. U-Boot loads and boots the kernel (main OS).

    U-Boot Components

    U-Boot works in two stages:

    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:

    CommandDescription
    helpList all commands
    printenvShow environment variables
    setenv var valueSet environment variable
    saveenvSave variables to flash
    bootBoot the OS
    loadLoad file into memory
    mmc, fatload, ext4loadLoad files from SD card (FAT or EXT4)
    bootmBoot a Linux kernel image (uImage)
    bootzBoot a zImage
    bootiBoot an Image format (used for ARM64)
    runRun a command script or bootcmd

    U-Boot Environment Variables

    These are key-value pairs that control how the board boots.

    bootcmd=run mmcboot
    bootargs=console=ttyO0,115200 root=/dev/mmcblk0p2 rw
    bootdelay=3
    
    • bootcmd – What command to run to boot the OS
    • bootargs – Kernel arguments (console, root filesystem, etc.)
    • bootdelay – Wait time (in seconds) before running bootcmd

    Booting from Different Sources

    U-Boot can boot from:

    • SD Card (mmc dev 0, fatload mmc 0)
    • NAND / NOR Flash
    • USB
    • Ethernet (TFTP Boot)

    Example: Boot Kernel from SD Card

    mmc dev 0
    fatload mmc 0:1 0x82000000 zImage
    fatload mmc 0:1 0x88000000 am335x-boneblack.dtb
    setenv bootargs console=ttyO0,115200 root=/dev/mmcblk0p2 rw
    bootz 0x82000000 - 0x88000000
    

    Explanation:

    • Load kernel to memory 0x82000000
    • Load Device Tree Blob (DTB)
    • Set bootargs
    • Boot with bootz

    U-Boot Compilation (Advanced but useful to know)

    You can compile U-Boot from source:

    Step 1: Get the Source

    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)
    

    You’ll get output files like:

    • MLO – SPL for initial boot
    • u-boot.img – Main U-Boot
    • u-boot.bin, u-boot.lds, etc.

    5. Customizing U-Boot Features

    A. Change Default Boot Command

    Edit include/configs/<your_board>.h:

    #define CONFIG_BOOTCOMMAND "run mmcboot"
    

    You can replace it with your custom script:

    #define CONFIG_BOOTCOMMAND "fatload mmc 0:1 0x82000000 zImage; bootz 0x82000000"
    

    B. Change Environment Variables

    Default environment is set in:

    include/env_default.h
    

    You can hardcode:

    ENV_DEFAULT_VARS {
      "bootargs=console=ttyS0,115200 root=/dev/mmcblk0p2 rw\0",
      "bootcmd=run customboot\0",
    }
    

    Or set variables at runtime:

    setenv bootdelay 5
    setenv bootargs console=ttyS0,115200 root=/dev/mmcblk0p2 rw
    saveenv
    

    C. Add Splash Screen (Logo on Boot)

    Steps:

    1. Enable splash screen in config: #define CONFIG_VIDEO #define CONFIG_SPLASH_SCREEN #define CONFIG_CMD_BMP
    2. Add BMP file to filesystem (e.g., splash.bmp)
    3. Load and show it from script: fatload mmc 0:1 0x82000000 splash.bmp bmp display 0x82000000

    D. Add Support for New Hardware (like GPIO, I2C)

    1. Enable driver in Kconfig or your board config header.
    2. For example, to enable I2C: In include/configs/<board>.h: #define CONFIG_SYS_I2C
    3. Add initialization code if needed in board/<vendor>/<board>/board.c.

    6. Rebuild and Flash U-Boot

    Rebuild after your changes:

    make clean
    make -j$(nproc)
    

    Flash MLO and u-boot.img to SD card or onboard flash:

    sudo dd if=MLO of=/dev/sdX bs=512 seek=1
    sudo dd if=u-boot.img of=/dev/sdX bs=512 seek=96
    

    Replace /dev/sdX with your SD card device!

    7. Test via Serial Console

    Connect serial (e.g., USB-UART):

    • Use Minicom or PuTTY.
    • Set baud rate: 115200
    • On boot, interrupt with a key (usually space or Enter).
    • Use printenv, setenv, run, boot to test your changes.

    8. Optional: Create Custom Boot Scripts

    Create a script (e.g., boot.scr) using mkimage:

    vim boot.cmd
    
    setenv bootargs console=ttyS0,115200 root=/dev/mmcblk0p2 rw
    fatload mmc 0:1 0x82000000 zImage
    bootz 0x82000000
    

    Compile it:

    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 DidDescription
    Got U-Boot sourcegit clone
    Chose board configmake defconfig
    Customized featuresEdited header files, bootcmd
    Enabled featuresSplash screen, drivers
    Rebuilt & flashedmake, dd
    Tested via serialUsed setenv, boot
    (Optional) Used scriptBoot 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:

    void (*kernel_entry)(void) = (void *)0x80008000;
    kernel_entry();
    

    And from this point, the main OS or application takes control.


    Real Example: U-Boot Bootloader Initialization (Simplified)

    U-Boot is a popular open-source bootloader. Its initialization path looks like:

    start.S → board_init_f() → board_init_r() → env init → run bootcmd → boot OS
    

    What happens:

    StepFunctionPurpose
    1start.SSetup low-level stuff (stack, memory)
    2board_init_f()Init DRAM, UART, GPIO, etc.
    3board_init_r()Load environment, filesystem
    4bootcmdLoad OS image from SD/eMMC
    5bootz or bootmBoot Linux kernel or other

    Example: Simple Bootloader Initialization Code in C

    void bootloader_main() {
        init_clock();
        init_uart();
        init_sdram();
        uart_print("Bootloader: Initialized successfully.\n");
    
        load_firmware_to_ram();
        jump_to_firmware();  // function pointer call
    }
    

    Where Is This Code Written?

    • 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

    ToolPurpose
    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 StageAction
    Power-OnCPU starts from fixed address
    Low-level InitStack, memory, .bss, vectors
    Board InitClock, UART, RAM, peripherals
    Load FirmwareRead from Flash or SD
    Jump to OSHandoff 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:

    1. Boot ROM (or Primary Bootloader) is executed from secure memory.
    2. It reads the firmware (e.g., u-boot.img, zImage).
    3. It verifies the signature using the stored public key.
    4. If valid → The boot continues.
    5. If invalid → Boot is aborted (or recovery mode is triggered).

    Visual Summary

    +---------------------------+
    | Power-On Reset            |
    +---------------------------+
              ↓
    +---------------------------+
    | Boot ROM / Secure Boot    |
    | (checks signature)        |
    +---------------------------+
          ↙             ↘
     [Valid Signature]     [Invalid Signature]
         ↓                       ↓
    +-------------+      +------------------+
    | Load U-Boot |      | Boot Fails /     |
    | or Kernel   |      | Recovery Mode    |
    +-------------+      +------------------+
    

    Secure Boot in U-Boot (Simplified)

    U-Boot can support secure boot with:

    • Verified Boot (CONFIG_FIT_SIGNATURE) using FIT images
    • Signing boot components with OpenSSL
    • Storing keys in ROM/eFuse/TPM

    Example steps:

    1. Create a signed FIT image: mkimage -f auto.its -k keys/ -r signed.itb
    2. Enable signature verification in U-Boot config.
    3. Burn public key hash in ROM/eFuse.

    Real World Use Cases

    DeviceSecure Boot Used?Purpose
    Android Phones✅ YesTo stop OS tampering
    Automotive ECUs✅ YesTo avoid hacking safety features
    IoT Devices✅ YesTo protect smart home security

    Benefits of Secure Boot

    FeatureBenefit
    Code integrityStops malicious firmware
    Trust chainEnsures only signed software runs
    Hardware-backedSecure even if software is compromised

    Without Secure Boot

    Imagine someone flashing malicious firmware into your smart device — they could:

    • Bypass authentication
    • Read private data
    • Brick the system
    • Spy or inject malware

    Tools Involved

    ToolUse
    opensslGenerate keys and signatures
    mkimageCreate signed U-Boot images
    fuse-toolsBurn keys into OTP/eFuse
    Secure boot loaderBuilt into SoC (like i.MX, TI, STM32, etc.)

    Summary Table

    TermMeaning
    BootloaderFirst code after power-on
    Secure BootPrevents unauthorized code from running
    SigningValidating firmware using cryptographic keys
    Trusted Key StorageROM, OTP, eFuse — unmodifiable memory
    FIT ImageFlattened Image Tree format used by U-Boot for signing

    For Beginners: What to Learn Next?

    • Basics of Cryptography (Hashing, RSA)
    • How to generate private/public keys
    • U-Boot’s CONFIG_FIT_SIGNATURE
    • Vendor-specific Secure Boot guides (e.g., STM32, NXP, TI)

    Step-by-Step Secure Boot Tutorial with U-Boot (BeagleBone Black)

    Prerequisites

    RequirementDetails
    BoardBeagleBone Black (BBB)
    Host PCUbuntu/Debian recommended
    U-Boot SourceFrom https://source.denx.de/u-boot/u-boot
    Toolsopenssl, mkimage, dtc, arm-cross-compiler

    Step 1: Set Up the Build Environment

    sudo apt update
    sudo apt install gcc-arm-linux-gnueabihf make u-boot-tools device-tree-compiler openssl
    

    Clone U-Boot:

    git clone https://source.denx.de/u-boot/u-boot.git
    cd u-boot
    

    Step 2: Configure U-Boot for BeagleBone Black

    make CROSS_COMPILE=arm-linux-gnueabihf- am335x_evm_defconfig
    

    Enable FIT image and signature verification in .config:

    make menuconfig
    

    Navigate and enable:

    • CONFIG_FIT_SIGNATURE
    • CONFIG_RSA
    • Save and exit.

    Step 3: Generate RSA Key Pair for Signing

    mkdir keys && cd keys
    
    # Generate 2048-bit private key
    openssl genpkey -algorithm RSA -out private.key -pkeyopt rsa_keygen_bits:2048
    
    # Extract public key
    openssl rsa -in private.key -pubout -out public.key
    

    Generate device tree source for key:

    mkimage -D "-I dts -O dtb -p 2000" -f auto.its signed.itb
    

    We’ll use the ITS file in a moment.

    Step 4: Prepare U-Boot Image and Kernel for Signing

    You need:

    • u-boot.itb (signed bootloader image)
    • zImage (kernel)
    • devicetree.dtb

    Prepare an .its file:

    /dts-v1/;
    / {
        description = "Secure Boot Image";
        images {
            kernel@1 {
                description = "Linux Kernel";
                data = /incbin/("zImage");
                type = "kernel";
                arch = "arm";
                os = "linux";
                compression = "none";
                load = <0x80008000>;
                entry = <0x80008000>;
                hash@1 {
                    algo = "sha256";
                };
            };
            fdt@1 {
                description = "Flattened Device Tree blob";
                data = /incbin/("am335x-boneblack.dtb");
                type = "flat_dt";
                arch = "arm";
                compression = "none";
                hash@1 {
                    algo = "sha256";
                };
            };
        };
        configurations {
            default = "conf@1";
            conf@1 {
                kernel = "kernel@1";
                fdt = "fdt@1";
                signature@1 {
                    algo = "rsa2048";
                    key-name-hint = "dev";
                    sign-images = "kernel@1", "fdt@1";
                };
            };
        };
    };
    

    Step 5: Sign the FIT Image

    Back in your project root:

    mkimage -f auto.its -k keys/ -r signed.itb
    

    This produces a signed FIT image.


    Step 6: Embed Public Key into U-Boot

    Edit your U-Boot device tree (e.g., u-boot.dts) to embed the public key:

    / {
        public-keys {
            dev_rsa {
                key-name-hint = "dev";
                rsa,rsa-n = /incbin/("keys/dev.key");
                rsa,rsa-e = [01 00 01];
            };
        };
    };
    

    Recompile U-Boot:

    make -j$(nproc) CROSS_COMPILE=arm-linux-gnueabihf-
    

    Step 7: Load and Boot FIT Image on Board

    1. Copy signed.itb to the SD card or TFTP directory.
    2. Boot U-Boot on your BeagleBone Black.
    3. In U-Boot console:
    ext4load mmc 0:1 0x82000000 signed.itb
    bootm 0x82000000
    

    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

    StageVerified?
    Boot ROM (SoC)Yes – hardwired
    SPL (Secondary Bootloader)Yes – optional signature
    U-Boot ProperYes
    Linux Kernel (FIT image)Yes
    Root FilesystemNo (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:

    1. NOR Flash – Faster read speeds, slower writes, often used for storing the bootloader and firmware.
    2. 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.
    • Example: sudo dd if=u-boot.img of=/dev/sdX bs=512 seek=1

    6. Testing the Bootloader

    • 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:

    1. Power-On: System powers up, and the CPU starts from ROM.
    2. Boot ROM: The Boot ROM checks for valid bootloader code in the flash memory.
    3. Bootloader Loads: The bootloader (e.g., U-Boot) is loaded into RAM.
    4. Bootloader Initialization: The bootloader configures hardware and prepares the system for loading the OS.
    5. Load Kernel: The bootloader reads the kernel image from flash and loads it into RAM.
    6. 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 

    Special thanks to @mr-raj for contributing to this article on EmbeddedPrep

  • Master SDLC Software Development Life Cycle using our Beginner-Friendly Guide 2026

    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.

    ✍️ Deliverable: Design documents, UML diagrams, UI mockups.

    ✅ Tip: Decide things like — Will it have a login system? What’s the database structure?

    3. Implementation (Coding) Phase

    Goal: Write the actual code.

    👨‍💻 Developers now take the designs and turn them into functional software by:

    • Writing source code
    • Creating databases
    • Connecting front-end and back-end systems

    ✍️ Deliverable: A working version (or parts) of the software.

    Tip: Each module (like login, cart, profile) is usually coded and tested individually.

    4. Testing & Integration

    Goal: Make sure everything works as expected.

    In this phase:

    • Testers check for bugs and issues (using manual or automated tools)
    • Developers fix the bugs
    • All modules are integrated and tested as a whole

    ✍️ Deliverable: A stable, bug-free software ready for deployment.

    Tip: This is like quality checking your food before serving — better to catch a bug now than let users find it!

    5. Maintenance Phase

    Goal: Keep the software running smoothly after release.

    🔧 After the software is launched:

    • New features might be added
    • Bugs that users find are fixed
    • Security updates are applied

    ✍️ Deliverable: Regular software updates and patches.

    ✅ Tip: Think of this like car servicing — even the best apps need maintenance to stay in shape!

    SDLC Models: Agile vs. Waterfall

    Let’s explore two common SDLC models: Agile and Waterfall.

    Waterfall Model (Traditional, Linear)

    A step-by-step model where each phase must finish before the next begins.

    Requirements ➡ Design ➡ Implementation ➡ Testing ➡ Maintenance
    

    Pros:

    • Simple and easy to follow
    • Good for smaller or clearly defined projects

    Cons:

    • Not flexible; changing requirements is hard
    • Bugs or issues are often discovered late

    Agile Model (Modern, Flexible)

    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

    FeatureWaterfall ModelAgile Model
    ApproachLinear (one-time flow)Iterative (repeats)
    FlexibilityLowHigh
    Customer InvolvementAt the beginning onlyOngoing, frequent
    Best ForSimple, fixed-scope projectsEvolving, complex projects
    Delivery StyleOne big releaseSmall, 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

    Requirements → Design → Implementation → Testing. Instead

    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:

    1. Individuals and interactions over processes and tools
    2. Working software over comprehensive documentation
    3. Customer collaboration over contract negotiation
    4. 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

    TermDescription
    User StoryA feature described from the user’s perspective.
    SprintA fixed-duration iteration (usually 1–4 weeks).
    BacklogA prioritized list of user stories.
    Scrum MasterFacilitates Scrum practices and removes blockers.
    Product OwnerOwns the product vision and backlog.
    Daily StandupA 15-minute meeting to sync up.
    Burndown ChartShows 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

    1. Jira – Widely used for Scrum/Kanban boards, user stories, sprints, and burndown charts.
    2. Trello – Visual task management using boards and cards (popular for small teams).
    3. Asana – Task tracking with project timelines and collaboration.
    4. ClickUp – Combines tasks, docs, and goals in one platform.
    5. VersionOne – Agile project management tool supporting SAFe and Scrum.
    6. Azure DevOps (formerly VSTS) – Integrated with Microsoft tools for Agile planning and CI/CD.

    Source Control & Code Management

    1. Git – Distributed version control system.
    2. GitHub / GitLab / Bitbucket – Platforms for Git repositories with built-in issue tracking and CI/CD.

    CI/CD & Build Automation

    1. Jenkins – Automates builds and deployments.
    2. CircleCI / Travis CI / GitLab CI – Integrates with Git to run automated pipelines.
    3. TeamCity – JetBrains tool for continuous integration and delivery.

    Testing Tools

    1. Selenium – Web application automation testing.
    2. JUnit / NUnit / TestNG – Unit testing frameworks.
    3. Postman – API testing.
    4. Cucumber – BDD tool for writing test cases in plain English.
    5. QTest – Test case management.

    Documentation & Knowledge Sharing

    1. Confluence – Wiki-style documentation often paired with Jira.
    2. Notion – All-in-one workspace for docs, tasks, and wikis.
    3. Google Docs/Sheets – Lightweight collaboration tools for real-time editing.

    Monitoring & Feedback

    1. Burndown/Burnup Charts – Usually generated within tools like Jira.
    2. Retrospective Tools – EasyRetro, FunRetro for sprint reviews.
    3. 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:

    1. 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.
    2. 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.
    3. 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.
    4. IBM Engineering Test Management (ETM)
      • Formerly: Rational Quality Manager (RQM)
      • Used for: Test planning, execution, and linking to requirements and development tasks.
    5. 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:

    1. Requirement Gathering & Analysis
    2. System Design
    3. Implementation (Coding)
    4. Testing
    5. Deployment
    6. 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 

    Special thanks to @mr-raj for contributing to this article on EmbeddedPr