Tag: embedded prep

  • Interrupts in Embedded Systems : A Complete Beginner’s Guide 2026

    Introduction

    Interrupts in Embedded Systems : An interrupt is a way for hardware or software to get the processor’s attention when something important happens. It’s like someone tapping you on the shoulder while you are busy working, saying, “Hey, look at this right now!”

    When an interrupt happens, the processor doesn’t stop immediately. It first finishes the instruction it is currently working on. After that, it quickly moves to a special piece of code called the Interrupt Service Routine (ISR) or Interrupt Handler.

    The ISR is like a small program that tells the processor what to do when a specific interrupt occurs. For example, if you press a button connected to your device, the processor will run the ISR to check which button was pressed and what action should be taken.

    In the world of embedded systems, interrupts are powerful tools that help a device respond quickly to important events. Instead of constantly checking (polling) whether something has happened, interrupts alert the processor immediately, saving time and power.

    What is an Interrupt?

    An interrupt is a signal that pauses the normal flow of a program and forces the CPU to run a special piece of code called an Interrupt Service Routine (ISR).

    After the ISR finishes, the CPU resumes its previous work as if nothing happened.

    In simple words:
    An interrupt is like someone tapping your shoulder to get your attention while you are busy working.

    Why are Interrupts Important?

    Interrupts offer several benefits in embedded systems:

    • Real-time Response: Immediate reaction to critical events like button presses, sensor readings, or incoming data.
    • Efficiency: No need to waste CPU time constantly checking for events.
    • Power Saving: Processor can stay in low-power modes and wake up only when needed.
    • Multitasking: Perform multiple tasks effectively without missing important signals.

    How Interrupts Work: Step-by-Step

    Here’s a simple flow of how interrupts work:

    1. Normal Execution: CPU is busy running the main program.
    2. Interrupt Event: An external or internal event occurs (like a button press).
    3. Interrupt Request: The event triggers an interrupt request to the CPU.
    4. Interrupt Handling:
      • CPU pauses the main program.
      • CPU saves the current program state (so it can return later).
      • CPU jumps to the Interrupt Service Routine (ISR).
    5. ISR Execution: Special code in ISR runs to handle the event.
    6. Return to Main Program: CPU restores the saved state and continues where it left off.

    Real-World Example of Interrupt

    Imagine a smart doorbell:

    • Normally, the microcontroller is managing Wi-Fi or saving battery in sleep mode.
    • When a visitor presses the doorbell button, it sends an interrupt.
    • The microcontroller wakes up immediately and rings the chime or sends a notification.

    Without interrupts, the microcontroller would have to keep checking the button all the time, wasting energy.

    Key Components of an Interrupt System

    • Interrupt Request (IRQ): The signal that notifies the CPU about an event.
    • Interrupt Vector Table (IVT): A table that holds the addresses of all ISRs.
    • Interrupt Service Routine (ISR): A short program that handles the interrupt.
    • Interrupt Controller: Manages multiple interrupts and prioritizes them if needed.

    Simple Code Example (Pseudocode)

    #include <some_microcontroller_library.h>
    
    // Define Interrupt Service Routine
    void button_press_ISR(void) {
        // Code to handle button press
        turnOnLED();
    }
    
    int main() {
        setupButtonInterrupt(button_press_ISR); // Link ISR to button press
        while (1) {
            // Main program continues
            doOtherTasks();
        }
    }
    

    Explanation:

    • setupButtonInterrupt() configures the button to generate an interrupt.
    • When the button is pressed, button_press_ISR() runs.
    • The main program (doOtherTasks()) keeps working without constantly checking the button.

    Best Practices When Using Interrupts

    • Keep ISR Short: The interrupt service routine should be as fast as possible.
    • Avoid Heavy Operations: No complex calculations or long delays inside the ISR.
    • Protect Shared Data: Use techniques like disabling interrupts or using volatile variables when accessing shared data.
    • Set Priorities Wisely: If multiple interrupts can occur, assign proper priorities.

    Types of Interrupts

    Interrupts are mainly divided into two big categories:

    • Hardware Interrupts
    • Software Interrupts

    And under these, there are some more types based on behavior.

    Let’s dive into each of them :

    1. Hardware Interrupts

    Definition:
    Hardware interrupts are triggered by external hardware devices like a keyboard, mouse, sensor, timer, or network card.

    Example:

    • Pressing a key on the keyboard
    • Clicking a mouse
    • Receiving data from a sensor

    When these hardware components want CPU attention, they send a signal to the CPU — “Hey, I need you now!”

    Types inside hardware interrupts:

    a) Maskable Interrupt

    • Meaning:
      These are interrupts that can be ignored (masked) by the CPU if needed.
    • Example:
      • You have a low-priority temperature sensor reading.
        CPU may postpone or disable it temporarily if a higher-priority task is running.
    • Real use:
      Useful for less urgent tasks.

    b) Non-Maskable Interrupt (NMI)

    • Meaning:
      These interrupts cannot be ignored by the CPU. They are critical.
    • Example:
      • Hardware failure like memory errors
      • Power failure warning
    • Real use:
      To handle very serious issues that must be fixed immediately to prevent damage.

    2. Software Interrupts

    Definition:
    Software interrupts are triggered by programs or instructions inside the CPU — not by external hardware.

    Example:

    • A program might intentionally request a service from the operating system, like asking to print a document 🖨️.

    How it works:
    The software uses a special instruction (often called INT) to generate an interrupt.

    Real-world Example:

    • A C program calling an operating system function like printf() indirectly uses a software interrupt to talk to the printer or screen.

    Special Classifications

    Apart from the basic two types, interrupts are often further classified based on how they are handled:

    3. Synchronous Interrupts

    • Meaning:
      Happen at predictable times, usually caused by the instruction flow of the program.
    • Example:
      • Division by zero error (mathematical mistake)
      • Invalid memory access (segmentation fault)
    • Real use:
      Catching software bugs or errors.

    4. Asynchronous Interrupts

    • Meaning:
      Happen at unpredictable times, triggered by external hardware.
    • Example:
      • User pressing a keyboard key at any moment.
      • Receiving network data.
    • Real use:
      Handling random real-world events.

    5. Vectored Interrupts

    • Meaning:
      CPU knows exactly where the Interrupt Service Routine (ISR) is located — because the address is predefined.
    • Example:
      • In ARM Cortex-M processors, vector tables store all ISR addresses.
    • Real use:
      Faster because CPU jumps directly to the correct code.

    6. Non-Vectored Interrupts

    • Meaning:
      CPU does not know directly where to go — it must find the ISR address first.
    • Example:
      • Older systems using interrupt controllers.
    • Real use:
      Slower but more flexible for dynamic interrupt management.

    Quick Summary Table

    Type of InterruptTriggered ByCan Be Masked?Predictability
    Hardware InterruptExternal deviceMaskable/Non-MaskableAsynchronous
    Software InterruptSoftware programGenerally MaskableSynchronous
    Synchronous InterruptProgram executionDependsPredictable
    Asynchronous InterruptExternal eventDependsUnpredictable
    Vectored InterruptPredefined address–Fast access
    Non-Vectored InterruptDynamic address–Slower

    Final Picture

    Think of interrupts like urgent notifications in your phone:

    • 🛎️ Hardware interrupts = Phone call from someone (external event)
    • 🛎️ Software interrupts = Reminder set by yourself (internal event)
    • 🔕 Maskable = You can put it on silent
    • 🔔 Non-Maskable = Emergency call — can’t ignore
    • 📍 Vectored = Direct link to caller
    • 🔍 Non-Vectored = You have to search who’s calling first

    What is an Interrupt Service Routine (ISR)?

    Imagine you are watching your favorite TV show and suddenly your phone rings.
    You pause the show, answer the call, finish talking, and then return to watching the show.

    This is exactly what an interrupt is in a microcontroller!

    • The TV show = Main program running normally
    • Phone call = Interrupt (something urgent needing immediate attention)
    • Answering the call = Executing the Interrupt Service Routine (ISR)
    • Returning to TV = Going back to the main program after handling the interrupt

    In technical words:
    An interrupt is a signal that temporarily stops the main program and forces the microcontroller to do something else immediately.

    When an interrupt happens, the microcontroller needs to know:

    • What to do?
    • Which code to run?

    The special piece of code that runs when an interrupt occurs is called the Interrupt Service Routine (ISR).

    ➡️ In short:
    ISR = A function that is automatically called when an interrupt happens.

    How Does the Microcontroller Know Which ISR to Run?

    Every interrupt has a fixed place in memory where the microcontroller looks to find the address of the ISR.
    This fixed list of addresses is called the Interrupt Vector Table.

    • Think of it like an emergency contact list.
    • Each type of interrupt (like Timer, UART, External Pin) has its own slot in this table.
    • The table points to the correct ISR for that interrupt.

    So, Interrupt Vector Table = List of addresses that point to ISRs.

    Step-by-Step Flow of What Happens During an Interrupt:

    1. Interrupt Occurs
      (Example: A timer overflow, a button press, or a sensor signal)
    2. Microcontroller Pauses Main Program
      (It saves where it was in the main code.)
    3. Microcontroller Looks at Interrupt Vector Table
      (Finds the address of the ISR.)
    4. Microcontroller Jumps to the ISR
      (Starts executing the interrupt service routine.)
    5. ISR Executes
      (Handles the emergency or event.)
    6. Return to Main Program
      (After ISR finishes, the microcontroller goes back exactly where it left off.)

    Important Points About ISRs

    • Fast and Short: ISRs should be quick. Don’t put slow operations inside ISR.
    • No Return Value: ISRs usually do not return any value.
    • Special Syntax: In some programming languages (like C), you have to tell the compiler that a function is an ISR (using special keywords or attributes).
    • Global Variables: If you change variables inside an ISR, be careful — you might need to use volatile keyword.

    Real Life Example

    Suppose you have a doorbell system:

    • The microcontroller is busy displaying the time on a screen.
    • When someone presses the doorbell button, it triggers an interrupt.
    • The microcontroller pauses updating the time, rings the bell (by running the ISR), and then returns to updating the time.

    Simple, right?

    In a Simple Diagram:

    Main Program --> [Interrupt Occurs] --> ISR Runs --> Main Program Continues

    Simple External Interrupt ISR in C

    Imagine:

    • You have a button connected to a GPIO pin (say Pin 2).
    • When the button is pressed, the microcontroller should turn ON an LED.

    C Code Example:

    #include <avr/io.h>      // Header for I/O functions (AVR microcontrollers)
    #include <avr/interrupt.h> // Header for Interrupt functions
    
    void setup()
    {
        // Set Pin 2 (PD2) as input (button)
        DDRD &= ~(1 << PD2);
    
        // Set Pin 5 (PB5) as output (LED)
        DDRB |= (1 << PB5);
    
        // Enable external interrupt INT0 (which is usually on PD2)
        EIMSK |= (1 << INT0);
    
        // Set interrupt on rising edge (button press)
        EICRA |= (1 << ISC01) | (1 << ISC00);
    
        // Enable global interrupts
        sei();
    }
    
    ISR(INT0_vect)
    {
        // This function will run when the button is pressed
        PORTB ^= (1 << PB5); // Toggle LED (ON if OFF, OFF if ON)
    }
    
    int main(void)
    {
        setup(); // Call the setup function
    
        while (1)
        {
            // Main program loop does nothing
            // Microcontroller waits here until interrupt happens
        }
    }
    

    Now let’s explain this code line-by-line:

    1. Header Files

    #include <avr/io.h> 
    #include <avr/interrupt.h>
    
    • avr/io.h: To control input/output pins.
    • avr/interrupt.h: To use interrupt-related functions and macros.

    2. Setup Function

    void setup()
    {
        // Set Pin 2 as Input
        DDRD &= ~(1 << PD2);
    
    • DDRD is the Data Direction Register for Port D.
    • PD2 means Pin 2 of Port D.
    • &= ~(1 << PD2) sets that pin as input.
        // Set Pin 5 as Output
        DDRB |= (1 << PB5);
    
    • DDRB is the Data Direction Register for Port B.
    • PB5 usually controls the on-board LED.
    • |= (1 << PB5) sets it as output.

    3. Enable External Interrupt

        EIMSK |= (1 << INT0);
    
    • EIMSK is the External Interrupt Mask Register.
    • INT0 is the external interrupt for Pin 2.
    • This enables external interrupt 0.

    4. Set Interrupt Trigger (Rising Edge)

        EICRA |= (1 << ISC01) | (1 << ISC00);
    
    • EICRA is the External Interrupt Control Register A.
    • ISC01 and ISC00 bits control how the interrupt is triggered.
    • Setting both = trigger interrupt on rising edge (button press).

    5. Enable Global Interrupts

        sei();
    
    • sei() function enables global interrupts.
    • Without this, no interrupt would happen!

    6. Interrupt Service Routine (ISR)

    ISR(INT0_vect)
    {
        PORTB ^= (1 << PB5);
    }
    
    • ISR(INT0_vect) defines the Interrupt Service Routine for External Interrupt 0.
    • Inside the ISR, we toggle the LED:
      • If LED was OFF, it turns ON.
      • If LED was ON, it turns OFF.

    7. Main Loop

    int main(void)
    {
        setup();
        while (1)
        {
            // Main code does nothing
        }
    }
    
    • We call setup() once to configure pins and interrupts.
    • Then, we sit in a forever loop.
    • The real work (turning the LED ON/OFF) happens inside the ISR when the button is pressed.

    Important Note:

    • Never put delay or slow code inside an ISR.
      (Because it blocks the microcontroller from responding to new interrupts!)
    • Keep ISRs short and fast!

    Final Big Picture Diagram:

    Button Press (on PD2) --> Trigger INT0 Interrupt --> Run ISR(INT0_vect) --> Toggle LED --> Return to Main Loop

    Quick Summary

    TermMeaning
    InterruptA sudden event that needs immediate attention
    ISRSpecial function that runs when an interrupt occurs
    Interrupt Vector TableList that stores addresses of all ISRs

    Why Do We Need to Enable or Disable Interrupts?

    • Enable interrupts when you want your system to listen and respond to external or internal events.
    • Disable interrupts temporarily if:
      • You are performing a critical task that should not be interrupted (example: updating shared memory).
      • You want to avoid conflicts between normal code and interrupt code.

    How to Enable and Disable an Interrupt?

    In general, enabling and disabling interrupts can happen in two ways:

    • Globally — affecting all interrupts.
    • Individually — affecting a specific interrupt.

    Let’s see both!

    Enabling and Disabling Global Interrupts

    When you disable global interrupts, all interrupts are temporarily ignored.
    When you enable global interrupts, the CPU starts accepting interrupts again.

    Common in C programming (Microcontrollers like AVR, ARM):

    // Disable all interrupts globally
    __disable_irq();  // For ARM Cortex (CMSIS standard)
    cli();            // For AVR (Atmel chips)
    
    // Enable all interrupts globally
    __enable_irq();   // For ARM Cortex
    sei();            // For AVR
    

    Notes:

    • cli() = Clear Interrupt Flag (disable all interrupts)
    • sei() = Set Interrupt Flag (enable all interrupts)

    Enabling and Disabling Specific Interrupts

    Sometimes you don’t want to disable all interrupts — only a specific one (like just disabling the timer interrupt).

    Example: Disabling specific interrupt in C

    // Assuming you're using an AVR microcontroller
    TIMSK0 &= ~(1 << TOIE0); // Disable Timer0 Overflow Interrupt
    
    // To enable it back
    TIMSK0 |= (1 << TOIE0);  // Enable Timer0 Overflow Interrupt
    
    • TIMSK0 is the Timer Interrupt Mask Register.
    • TOIE0 is the Timer Overflow Interrupt Enable bit.

    Important Best Practices for Beginners

    • Only disable interrupts for a very short time (few microseconds) to avoid missing important events.
    • Always re-enable interrupts after your critical code section.
    • Never disable interrupts during long tasks like delays, communication, etc.

    Example of correct usage:

    __disable_irq();   // Disable interrupts
    
    // Critical section starts
    shared_variable = 10;  // Safely update
    // Critical section ends
    
    __enable_irq();    // Enable interrupts
    

    What Happens if You Forget to Enable Interrupts?

    If you disable interrupts and forget to enable them again:

    • Your system may stop responding to events.
    • Timers, communication (UART, SPI, I2C), and other peripherals may fail.
    • It can freeze parts of your program.

    Always double-check: if you disable interrupts, enable them back!

    Summary for Enable / Disable Interrupts

    TaskC Code (ARM Cortex)C Code (AVR)
    Disable all interrupts__disable_irq();cli();
    Enable all interrupts__enable_irq();sei();
    Disable specific interruptClear relevant bit in interrupt mask registerSame
    Enable specific interruptSet relevant bit in interrupt mask registerSame

    Interrupt Priority in 8051 Microcontroller

    Interrupt Priority means deciding which interrupt should be serviced first if two or more interrupts happen at the same time.

    The 8051 microcontroller allows us to assign priorities to each interrupt:

    • Higher priority interrupt gets serviced first.
    • Lower priority interrupt waits until higher priority one finishes.

    Thus, even if multiple events occur together, the CPU always knows which one is more important to handle first.

    How Many Interrupts Are There in 8051?

    The standard 8051 microcontroller has five interrupt sources:

    Interrupt NameTypeDescription
    INT0ExternalExternal Interrupt 0 (P3.2 pin)
    Timer 0InternalTimer 0 Overflow
    INT1ExternalExternal Interrupt 1 (P3.3 pin)
    Timer 1InternalTimer 1 Overflow
    SerialInternalSerial Communication (Receive/Transmit Complete)

    How is Interrupt Priority Set in 8051?

    In 8051, interrupt priorities are set using a special register called the Interrupt Priority Register (IP).

    Each bit of the IP register corresponds to one interrupt:

    • Bit = 1 → High Priority
    • Bit = 0 → Low Priority

    Structure of IP Register:

    BitNameInterrupt
    7–(Reserved)
    6–(Reserved)
    5PSSerial Port
    4PT1Timer 1
    3PX1External Interrupt 1
    2PT0Timer 0
    1PX0External Interrupt 0
    0–(Reserved)

    Example:

    IP = 0x10; // Set Timer 1 (PT1) as high priority
    

    Default Interrupt Priority Order (Without Manual Setting)

    If you don’t set anything manually, the default priority order is:

    1. External Interrupt 0 (INT0)
    2. Timer 0 Overflow
    3. External Interrupt 1 (INT1)
    4. Timer 1 Overflow
    5. Serial Communication

    👉 Lower number = Higher default priority

    Example: Practical Understanding

    Suppose:

    • External Interrupt 0 (INT0) is set to high priority.
    • Timer 1 Overflow is set to low priority.

    If both interrupts occur together:

    • INT0 will be serviced immediately.
    • Timer 1 must wait until INT0’s ISR is completed.

    Can Higher Priority Interrupt Interrupt an Ongoing ISR?

    ✅ Yes!
    If the CPU is executing a low-priority ISR and a high-priority interrupt occurs, the CPU pauses the low-priority ISR, services the high-priority ISR, and then returns back to complete the low-priority ISR.

    But if two interrupts have the same priority, no interrupt nesting happens. The CPU finishes the current ISR first and then services the next interrupt.

    Comparison between Interrupts and Polling:

    AspectInterruptsPolling
    DefinitionCPU is alerted by hardware when attention is needed.CPU repeatedly checks the device status at regular intervals.
    CPU UsageEfficient; CPU works on other tasks until interrupted.Inefficient; CPU wastes cycles checking status.
    Response TimeImmediate (when interrupt occurs).Depends on polling frequency; can be delayed.
    System EfficiencyHigh, as CPU can perform other tasks.Low, especially if polling rate is high.
    ComplexityMore complex to implement (needs ISR – Interrupt Service Routine).Simpler to implement.
    Power ConsumptionLower (saves power by sleeping/waiting).Higher (continuous checking uses more power).
    SuitabilityBest for time-critical applications.Suitable for simple, non-time-critical tasks.
    Example Use CaseKeyboard input handling, real-time systems.Reading sensor data at fixed intervals.

    Interrupt Triggering Methods

    Interrupts can be triggered in multiple ways, each with its own behavior and use case:

    1. Level Triggered Interrupts

    Level-triggered interrupts occur when a signal stays high (or low) for a certain period. The interrupt is triggered as long as the level condition is met.

    • Example: A button connected to the GPIO pin. If the button is pressed, the voltage level changes, and the interrupt is triggered as long as the button remains pressed.

    2. Edge Triggered Interrupts

    Edge-triggered interrupts occur when the signal changes state, such as from low to high (rising edge) or high to low (falling edge). These are often used when you only want to respond once to a signal transition.

    • Example: An external clock or pulse generator. You might want to trigger an interrupt every time the signal transitions from low to high.

    3. Both Edge Triggered Interrupts

    Some systems support interrupts on both rising and falling edges. This means the interrupt will be triggered whenever the signal transitions from low to high or high to low.

    • Example: A system that needs to respond to every clock pulse, whether it’s rising or falling.

    Setting Up Interrupts in Embedded Systems

    Setting up interrupts typically involves the following steps:

    1. Configure the Interrupt: Set the interrupt priority, triggering method (rising edge, falling edge, level triggered), and the pin to which the interrupt is connected.
    2. Enable the Interrupt: Enable the interrupt in the processor’s interrupt controller to start listening for the event.
    3. Write an Interrupt Service Routine (ISR): The ISR is the function executed when the interrupt occurs. The ISR should be as fast as possible to avoid interrupting other important processes.
    4. Enable Global Interrupts: After setting up the individual interrupt, enable global interrupts in the system to allow interrupts to occur.

    Example Code for Setting up an Interrupt in C (Embedded Systems)

    #include <avr/io.h>
    #include <avr/interrupt.h>
    
    void init_interrupt()
    {
        // Set INT0 (external interrupt 0) to trigger on the rising edge
        EICRA |= (1 << ISC01) | (1 << ISC00);   // ISC01 and ISC00 bits for rising edge
        EIMSK |= (1 << INT0);                    // Enable INT0 interrupt
        sei();                                   // Enable global interrupts
    }
    
    ISR(INT0_vect)
    {
        // Interrupt Service Routine (ISR) for INT0
        // This code will execute when the interrupt occurs
        PORTB ^= (1 << PB0);  // Toggle LED on PB0
    }
    
    int main()
    {
        DDRB |= (1 << PB0);   // Set PB0 as output for LED
        init_interrupt();      // Initialize interrupt
        while (1)
        {
            // Main program loop
            // The program will continue running while waiting for interrupts
        }
    }
    

    Common Interrupts in Embedded Systems

    • Timer Interrupts: Used to trigger an interrupt after a specific time delay.
    • External Interrupts: Triggered by external events such as a button press or signal change on a GPIO pin.
    • USART Interrupts: Triggered when there is data received or transmitted via UART/USART.
    • ADC Interrupts: Triggered when an Analog-to-Digital Conversion (ADC) is completed.

    Best Practices for Handling Interrupts

    1. Keep ISRs Short and Efficient: Interrupts should be serviced as quickly as possible to avoid blocking other interrupts or tasks.
    2. Minimize Global Variables in ISR: Avoid using global variables directly in the ISR. If needed, mark them volatile to prevent optimization issues.
    3. Use Interrupt Priorities: In complex systems, interrupt priorities ensure that more important interrupts are processed first.
    4. Disable Interrupts When Necessary: In critical sections of your code, disable interrupts temporarily to avoid unexpected behavior.

    Differentiating the level-triggered and edge-triggered interrupt methods:

    AspectLevel-Triggered InterruptsEdge-Triggered Interrupts
    Triggering ConditionTriggered when the signal remains at a specific level (high or low) for a period of time.Triggered when there is a transition in the signal, either from low to high (rising edge) or from high to low (falling edge).
    BehaviorInterrupt remains active as long as the condition (level) is met.Interrupt is triggered only once per signal transition.
    ExamplesA button press (signal stays high or low while the button is pressed).A pulse generator (interrupt triggered on each pulse).
    Type of SignalContinuous (signal level stays constant).Discrete (signal changes state).
    Use CaseTypically used for continuous monitoring of a signal state (e.g., power-on signals, continuous sensor readings).Used for detecting specific events like edges of clock signals, pulse counting, or event-driven systems.
    ComplexitySimpler to implement but may result in longer interrupt durations.More complex but allows for precise handling of events at the signal transition.
    Example ApplicationButton press detection, monitoring constant voltage levels.Clock edge detection, signal pulse measurement.

    Frequently Asked Questions (FAQ) on Embedded System Interrupts

    Q1: What is an interrupt in embedded systems?

    • An interrupt is a mechanism by which a processor’s normal execution flow is interrupted to handle an event or a condition that needs immediate attention. It allows the processor to deal with high-priority tasks in real-time.

    Q2: What are the types of interrupts?

    • Hardware Interrupts: Triggered by external devices like sensors or timers, which notify the processor of the need for attention.
    • Software Interrupts: Generated by software instructions to request specific services, such as system calls.

    Q3: What are interrupt vectors?

    • Interrupt vectors are memory addresses associated with each interrupt. When an interrupt occurs, the processor looks up the corresponding interrupt vector to find the appropriate service routine.

    Q4: What is an interrupt vector table (IVT)?

    • The Interrupt Vector Table (IVT) is a table of addresses that defines where the processor should jump when an interrupt occurs. It contains the addresses of interrupt service routines (ISR) for each interrupt source.

    Q5: What is an Interrupt Service Routine (ISR)?

    • An ISR is a function or routine that is executed when a specific interrupt occurs. It handles the interrupt, performs necessary tasks, and then returns control back to the main program.

    Q6: What is interrupt priority?

    • Interrupt priority determines the order in which multiple interrupts are handled. High-priority interrupts are handled before low-priority ones, ensuring time-sensitive tasks are addressed first.

    Q7: What are nested interrupts?

    • Nested interrupts allow a higher-priority interrupt to interrupt the ISR of a lower-priority interrupt. This ensures that critical tasks can preempt less important tasks.

    Q8: How does interrupt masking work?

    • Interrupt masking is used to disable or enable specific interrupts. The processor may mask certain interrupts to prevent them from interrupting the current process, which can be useful for critical sections in the code.

    Q9: What is the difference between edge-triggered and level-triggered interrupts?

    • Edge-triggered interrupts: Triggered by a change in the signal (either rising or falling edge).
    • Level-triggered interrupts: Triggered when the interrupt signal remains at a certain level (high or low) for a period of time.

    Q10: How are interrupts managed in microcontrollers?

    • Microcontrollers usually have a dedicated interrupt controller (such as a Nested Vectored Interrupt Controller – NVIC) to handle and prioritize interrupts. This controller determines the response and execution order for interrupts.

    Q11: What are the common uses of interrupts in embedded systems?

    • Interrupts are used in time-sensitive applications like handling external sensors, timers, communication protocols (e.g., UART, SPI, I2C), and real-time processing.

    Q12: What is interrupt latency?

    • Interrupt latency is the time taken by the processor to start executing an ISR after an interrupt has occurred. Lower latency is critical for real-time applications.

    Q13: What is the importance of context switching in interrupt handling?

    • Context switching refers to saving the state of the current process and loading the state of the ISR when an interrupt occurs. It ensures that the main program can resume after the ISR is executed.

    Q14: How do you avoid interrupt handling issues like jitter and race conditions?

    • Proper synchronization techniques such as disabling interrupts, using flags, or employing atomic operations can help avoid issues like jitter (variability in interrupt response time) and race conditions (conflict between interrupt and main program).

    Q15: What is the difference between polling and interrupt-based systems?

    • Polling: The processor continuously checks if an event has occurred.
    • Interrupt-based: The processor is alerted to an event only when it happens, reducing unnecessary CPU usage.

    Q16: What are interrupt-driven I/O systems?

    • In interrupt-driven I/O systems, the processor is interrupted when I/O operations (such as reading from a sensor or writing to a peripheral) are ready to be processed, instead of constantly checking the status.

    Q17: How do you implement an interrupt in an embedded system?

    • Steps include:
      1. Enable the interrupt source (e.g., a peripheral or timer).
      2. Configure the interrupt vector table.
      3. Write the ISR to handle the interrupt.
      4. Enable global interrupts.
      5. Ensure proper return from the ISR to resume normal execution.

    You can also Visit other tutorials of Embedded Prep 

    Special thanks to @embedded-prep for contributing to this article on Embedded Prep

  • Python try-except Made Easy: Handling Errors Like a Pro (2026)

    Python try-except : When you are writing Python programs, sometimes things go wrong. Maybe you tried to open a file that doesn’t exist, or divided a number by zero.
    Without handling these mistakes, your program will crash!
    That’s where try-except comes in — your superhero cape for handling errors gracefully.

    What is try-except?

    try-except is a way to tell Python:

    “Hey, try to run this code.
    But if something goes wrong, don’t crash. Instead, do this other thing.”

    Simple, right?

    Basic Structure

    try:
        # code that might cause an error
    except:
        # code that runs if there is an error
    

    Example 1: Handling Division

    try:
        number = int(input("Enter a number: "))
        result = 10 / number
        print(f"Result is: {result}")
    except:
        print("Oops! Something went wrong.")
    

    ✅ If you enter 2, it prints Result is: 5.0.
    ❌ If you enter 0, it says Oops! Something went wrong. (because you can’t divide by zero!)

    Why Use try-except?

    • Prevent crashing
    • Give friendly messages to users
    • Handle specific problems differently

    Imagine your app crashing just because someone entered a wrong value… Not good! 🙈

    Catching Specific Errors

    You can catch specific types of errors!
    This is more professional and safe.

    Example:

    try:
        number = int(input("Enter a number: "))
        result = 10 / number
        print(f"Result is: {result}")
    except ZeroDivisionError:
        print("Cannot divide by zero!")
    except ValueError:
        print("Please enter a valid number.")
    

    👉 Now, Python knows what went wrong and responds smartly!

    Bonus Tip: else and finally

    You can add two more blocks:

    • else: → Runs if no errors happen.
    • finally: → Always runs no matter what.

    Example:

    try:
        number = int(input("Enter a number: "))
        result = 10 / number
    except ZeroDivisionError:
        print("Cannot divide by zero!")
    except ValueError:
        print("Please enter a valid number.")
    else:
        print(f"Success! The result is {result}")
    finally:
        print("Thanks for using our calculator!")
    
  • Master Positional Parameters in Shell Scripting : Mastering $1, $2, $#, $@ (2026)

    What Are Positional Parameters in Shell Scripting?

    Positional Parameters : Bash or shell scripting, positional parameters allow you to access arguments passed to a script or function. They are incredibly useful when you want to handle user inputs dynamically.

    Why Learn Positional Parameters?

    • 📦 Helps you write flexible scripts
    • 🧑‍💻 Makes your scripts accept inputs
    • 🔁 Supports automation and reusability
    • 🧩 Essential for system scripting and DevOps

    Key Positional Parameters Explained

    ParameterMeaning
    $1, $2, …First, second, etc., arguments
    $0Name of the script
    $#Total number of arguments
    $@All arguments as separate words
    $*All arguments as a single word
    "$@"Preserves spaces and treats arguments individually
    "$*"Merges all arguments into one string

    Basic Script Example

    #!/bin/bash
    
    echo "Script Name: $0"
    echo "First Argument: $1"
    echo "Second Argument: $2"
    echo "Total Arguments: $#"
    echo "All Arguments (separately): $@"
    echo "All Arguments (together): $*"

    Run the script like this:

    bash myscript.sh apple banana cherry
    

    Output:

    Script Name: myscript.sh
    First Argument: apple
    Second Argument: banana
    Total Arguments: 3
    All Arguments (separately): apple banana cherry
    All Arguments (together): apple banana cherry
    

    Using "${@}" vs "${*}"

    Let’s see the difference:

    Example Script:

    #!/bin/bash
    
    echo "Using \"\$@\":"
    for arg in "$@"; do
        echo "$arg"
    done
    
    echo "Using \"\$*\":"
    for arg in "$*"; do
        echo "$arg"
    done
    

    Run with:

    bash myscript.sh "hello world" "foo bar"
    

    Output:

    Using "$@":
    hello world
    foo bar
    
    Using "$*":
    hello
    world
    foo
    bar
    

    📌 "$@" keeps the full argument structure. "$*" splits everything.

    Bonus: Accessing All Arguments with a Loop

    #!/bin/bash
    
    echo "You passed $# arguments."
    
    i=1
    for arg in "$@"; do
        echo "Argument $i: $arg"
        ((i++))
    done
    

    Best Practices

    • Always quote your parameters: "$1", "$@"
    • Use $# to validate inputs
    • Use loops with "$@" for safe iteration

    Real-world Use Case: Validate Arguments

    #!/bin/bash
    
    if [ "$#" -ne 2 ]; then
        echo "Usage: $0 <filename> <username>"
        exit 1
    fi
    
    echo "Creating file $1 for user $2..."
    

    5 Practical Examples Using Positional Parameters in Shell Scripts

    Example 1: Greeting Script with $1 and $2

    #!/bin/bash
    
    echo "Hello, $1!"
    echo "Welcome to $2."
    

    Run:

    bash greet.sh Nish India
    

    Output:

    Hello, Nish!
    Welcome to India.
    

    Example 2: Check If Arguments Are Passed Using $#

    #!/bin/bash
    
    if [ "$#" -lt 2 ]; then
      echo "❌ Error: At least 2 arguments required."
      echo "Usage: $0 <arg1> <arg2>"
      exit 1
    fi
    
    echo "You passed $#: $1 and $2"
    

    Run:

    bash check.sh first

    Output:

    ❌ Error: At least 2 arguments required.
    Usage: check.sh <arg1> <arg2>

    Example 3: Loop Through All Arguments Using $@

    #!/bin/bash
    
    echo "📦 List of arguments:"
    for item in "$@"; do
      echo "- $item"
    done

    Run:

    bash list.sh Mango Banana Apple

    Output:

    📦 List of arguments:
    - Mango
    - Banana
    - Apple

    Example 4: Sum Two Numbers Using Positional Parameters

    #!/bin/bash
    
    num1=$1
    num2=$2
    sum=$((num1 + num2))
    
    echo "The sum of $num1 and $num2 is: $sum"

    Run:

    bash sum.sh 5 15

    Output:

    The sum of 5 and 15 is: 20

    Example 5: Handle All Arguments as One String Using $*

    #!/bin/bash
    
    echo "🧵 All arguments as one string: $*"

    Run:

    bash all.sh "one two" "three four"

    Output:

    All arguments as one string: one two three four

    Summary Table

    SymbolWhat It DoesExample
    $1First argumentecho $1
    $2Second argumentecho $2
    $#Argument countecho $#
    $@All arguments (safe loop)for i in "$@"
    $*All arguments (single string)echo $*

    Conclusion

    Mastering positional parameters in shell scripting is essential for writing interactive, powerful, and reusable scripts. Always remember:

    • $1, $2, etc. — access specific inputs
    • $# — count of arguments
    • "$@" — iterate safely
    • Use "$*" when combining arguments into a single string

    Frequently Asked Questions (FAQ)

    Q1: What are positional parameters in shell scripting?

    Positional parameters are special variables in shell scripts that store command-line arguments passed to the script. For example, $1 is the first argument, $2 is the second, and so on.

    Q2: What does $0 mean in a shell script?

    $0 represents the name of the script itself. It’s often used in usage messages or logs to display which script is running.

    Q3: What is the difference between $@ and $*?

    • $@ treats each argument as a separate string, ideal for loops.
    • $* treats all arguments as one single string, often used in quotes.
      Example:
    for arg in "$@"; do echo "$arg"; done   # Handles each argument correctly
    for arg in "$*"; do echo "$arg"; done   # May split arguments incorrectly
    

    Q4: How do I check how many arguments were passed to the script?

    Use $# to get the number of arguments.

    echo "You passed $# arguments."

    Q5: Can I access more than 9 arguments like $10, $11, etc.?

    Yes, but use braces to avoid ambiguity.

    echo "Tenth argument: ${10}"

    Q6: How can I loop through all arguments in a script?

    for arg in "$@"; do
      echo "Argument: $arg"
    done
    

    This safely loops through all arguments with proper quoting.

    Q7: What happens if an argument is missing in $1, $2, etc.?

    It returns an empty string. Always validate input count using $# before accessing specific arguments.

    Q8: Are positional parameters only available in Bash?

    No, they’re available in all POSIX-compliant shells, including sh, Bash, Zsh, and Ksh.

    Q9: How can I print all arguments including the script name?

    echo "Script: $0"
    echo "Arguments: $@"

    Q10: Can I shift positional parameters inside a script?

    Yes! Use shift to move parameters leftward:

    shift
    echo "Now \$1 is: $1"

    You can also Visit other tutorials of Embedded Prep 

    Special thanks to @embedded-prep for contributing to this article on Embedded Prep

  • Master CPP Design Patterns Tutorial for Beginners 2026

    What Are Ccpp Design Patterns?

    CPP Design Patterns : Design patterns are proven solutions to common software design problems. They help developers write reusable, maintainable, and scalable code. Think of them as blueprints that guide how to structure your code in a way that solves recurring challenges.

    Why Learn Design Patterns in C++?

    C++ is widely used for system programming, game development, embedded systems, and real-time applications. Learning design patterns in C++ allows you to:

    • Write cleaner and more modular code
    • Improve collaboration in teams
    • Understand industry-level software design
    • Crack technical interviews easily

    Types of Design Patterns

    Design patterns are mainly divided into 3 categories:

    TypeDescription
    CreationalDeal with object creation
    StructuralFocus on object composition
    BehavioralHandle object interaction and communication

    Examples of Each:

    • Creational: Singleton, Factory Method
    • Structural: Adapter, Decorator
    • Behavioral: Observer, Strategy

    Real-World Analogy

    Imagine you’re building a house:

    • Creational Pattern is like choosing how you build rooms (standard floor plan or customized).
    • Structural Pattern is like how rooms are connected (walls, doors).
    • Behavioral Pattern is like how people in rooms communicate (talking, signaling).

    Tools You Need

    To follow this C++ design pattern tutorial series, you need:

    • A basic understanding of C++ syntax
    • An IDE like Code::Blocks, VS Code, or CLion
    • A compiler like g++

    C++ Design Patterns Table

    CategoryPattern NamePurpose
    CreationalSingletonEnsures a class has only one instance and provides a global access point.
    Factory MethodDefines an interface for creating an object, but lets subclasses alter the type.
    Abstract FactoryProvides an interface to create families of related objects.
    BuilderConstructs a complex object step by step.
    PrototypeCreates new objects by copying an existing one.
    StructuralAdapterConverts one interface into another expected by clients.
    BridgeSeparates abstraction from implementation so they can vary independently.
    CompositeComposes objects into tree structures to represent part-whole hierarchies.
    DecoratorDynamically adds behavior to objects.
    FacadeProvides a unified interface to a set of interfaces in a subsystem.
    FlyweightShares objects to support large numbers efficiently.
    ProxyProvides a surrogate or placeholder for another object.
    BehavioralObserverNotifies all dependent objects of any changes to another object.
    StrategyAllows selecting an algorithm at runtime.
    CommandEncapsulates a request as an object.
    StateAllows an object to change its behavior when its internal state changes.
    Template MethodDefines the program skeleton, letting subclasses fill in the steps.
    IteratorProvides a way to access elements of a collection sequentially.
    MediatorDefines an object that coordinates interaction between other objects.
    MementoCaptures and restores an object’s internal state.
    VisitorAdds operations to classes without modifying them.
    InterpreterInterprets sentences in a language using a defined grammar.
    Chain of ResponsibilityPasses a request through a chain of handlers.


    C++ Adding Two Numbers

    #include <iostream>
    
    class CalculatorSingleton {
    private:
        // Private constructor to prevent instantiation
        CalculatorSingleton() {}
    
        // Delete copy constructor and assignment operator
        CalculatorSingleton(const CalculatorSingleton&) = delete;
        CalculatorSingleton& operator=(const CalculatorSingleton&) = delete;
    
    public:
        // Public method to get the single instance
        static CalculatorSingleton& getInstance() {
            static CalculatorSingleton instance;
            return instance;
        }
    
        // Method to add two numbers
        int add(int a, int b) {
            return a + b;
        }
    };
    
    int main() {
        // Get the singleton instance
        CalculatorSingleton& calc = CalculatorSingleton::getInstance();
    
        // Add two numbers
        int result = calc.add(10, 20);
    
        // Print the result
        std::cout << "The sum is: " << result << std::endl;
    
        return 0;
    }
    

    🔍 Output:

    The sum is: 30

    Frequently Asked Questions (FAQs)

    What are design patterns in C++?

    Design patterns in C++ are reusable solutions to common software design problems. They represent best practices used by experienced developers to solve recurring problems in software architecture and object-oriented design.

    Why should I learn design patterns in C++?

    Learning design patterns helps you:

    • Write clean, reusable, and maintainable code
    • Understand and apply industry-standard practices
    • Improve problem-solving and architecture design skills
    • Prepare for technical interviews and real-world development

    Is this tutorial suitable for complete beginners?

    Yes! This tutorial is specially designed for beginners. It explains design patterns in simple language, with easy-to-understand examples and real-world use cases in C++.

    What topics will be covered in this C++ design patterns series?

    The tutorial will cover:

    • Introduction to Design Patterns
    • Types of Design Patterns (Creational, Structural, Behavioral)
    • Step-by-step implementation of each pattern in C++
    • Best practices and use cases

    Do I need to be an expert in C++ to follow this tutorial?

    No. You just need to have a basic understanding of C++, such as classes, objects, inheritance, and functions. The tutorial is crafted for beginners and will guide you through each concept.

    Are there code examples and explanations for each pattern?

    Yes! Every design pattern in this series comes with:

    • Simple code examples in C++
    • Detailed explanations
    • Diagrams (where needed)
    • Real-world applications

    Will this help in job interviews?

    Absolutely. Many software engineering interviews include questions on design patterns. Understanding these concepts will give you a competitive edge during technical interviews.

    How can I practice these design patterns?

    Each tutorial includes:

    • Hands-on exercises
    • Code snippets you can run and modify
    • Mini-project ideas to build confidence

    Is this Master CPP Design tutorials free?

    Yes, the entire Cpp Design Patterns Tutorial for Beginners is free to read and follow. No sign-up or payment is required.

    You can also Visit other tutorials of Embedded Prep 

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

  • L – Liskov Substitution Principle (LSP) | Master CPP Design Patterns 2026

    Liskov Substitution Principle (LSP)

    The Liskov Substitution Principle is one of the SOLID principles of object-oriented programming. It was introduced by Barbara Liskov in 1987 and it states:

    “Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.”

    What It Really Means:

    Imagine you’re working with a class called Bird that has a method fly(). If you create a subclass Penguin from Bird, but Penguin can’t fly, then substituting Bird with Penguin could break your program. That’s a violation of LSP.

    The principle pushes us to design subclasses that truly behave like their parent class, so code that uses the base class doesn’t have to worry about the specific subclass being used.

    LSP in Action (Simple Example):

    class Bird {
    public:
        virtual void fly() {
            std::cout << "Flying..." << std::endl;
        }
    };
    
    class Sparrow : public Bird {
    public:
        void fly() override {
            std::cout << "Sparrow flying!" << std::endl;
        }
    };
    
    // This class violates LSP
    class Ostrich : public Bird {
    public:
        void fly() override {
            throw std::logic_error("Ostriches can't fly!");
        }
    };
    

    In the example above, Ostrich breaks LSP because it can’t perform the fly() behavior expected from Bird.

    Why It Matters:

    • Encourages safe inheritance.
    • Makes code more reliable and maintainable.
    • Helps avoid unexpected runtime errors.
    • Ensures that your class hierarchy makes logical sense.

    How to Stick to LSP:

    • Don’t override methods in a way that changes their expected behavior.
    • Use composition over inheritance if subclass behavior doesn’t match the base class.
    • Clearly define contracts (what methods are supposed to do) and ensure subclasses respect them.

    What is LSP?

    Definition by Barbara Liskov:

    If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program.

    Simple Version:

    A subclass should be able to stand in for its parent class without breaking the program.

    What does that mean?

    Let’s break it down:

    • You create a base class (Parent class).
    • Then you make a derived class (Child class).
    • The child should behave in a way that’s expected from the parent.
    • You should be able to use the child wherever you used the parent, and things should just work.

    Real-Life Analogy

    Let’s say:

    • You have a class “Bird” with a method fly().
    • You create a subclass “Parrot” — fine, it can fly.
    • Now you create a subclass “Penguin” — 🐧 uh-oh! Penguins can’t fly.

    If you call fly() on a Penguin object assuming it’s a Bird, your program breaks the expectation.

    🚫 That violates LSP!

    Example in C++ that Violates LSP

    class Bird {
    public:
        virtual void fly() {
            cout << "Flying..." << endl;
        }
    };
    
    class Sparrow : public Bird {
    public:
        void fly() override {
            cout << "Sparrow flying" << endl;
        }
    };
    
    class Ostrich : public Bird {
    public:
        void fly() override {
            throw runtime_error("Ostriches can't fly!");
        }
    };
    

    Problem:

    • If someone uses Bird* b = new Ostrich(); b->fly(); expecting it to fly — it crashes.
    • Ostrich is a Bird, but it doesn’t behave like a Bird should.

    This violates LSP.

    LSP-Compliant Design

    Solution: Refactor your classes to respect behavior expectations.

    class Bird {
    public:
        virtual void eat() {
            cout << "Bird eating" << endl;
        }
    };
    
    class FlyingBird : public Bird {
    public:
        virtual void fly() = 0;
    };
    
    class Sparrow : public FlyingBird {
    public:
        void fly() override {
            cout << "Sparrow flying" << endl;
        }
    };
    
    class Ostrich : public Bird {
        // No fly() here
    };
    

    Now:

    • Ostrich doesn’t pretend to be a flying bird.
    • If your logic requires only flying birds, you use the FlyingBird type.
    • LSP is safely respected.

    LSP in Code: A Simple, Clear Example

    class Rectangle {
    public:
        virtual void setWidth(int w) { width = w; }
        virtual void setHeight(int h) { height = h; }
        virtual int getArea() { return width * height; }
    
    protected:
        int width;
        int height;
    };
    

    Now we make a Square from Rectangle:

    class Square : public Rectangle {
    public:
        void setWidth(int w) override {
            width = height = w;
        }
    
        void setHeight(int h) override {
            width = height = h;
        }
    };
    

    Problem:

    If someone does this:

    Rectangle* r = new Square();
    r->setWidth(5);
    r->setHeight(10);
    cout << r->getArea();  // Expected: 50, But it gives 100
    

    Uh-oh! Because setting width or height sets both in Square.
    This breaks expected behavior.
    It violates LSP.

    LSP-Compliant Refactor

    Instead, don’t inherit Square from Rectangle directly if they behave differently.

    Maybe use composition or separate hierarchy.

    LSP Summary Table

    ConceptBad (Violates LSP)Good (Follows LSP)
    Penguin inherits Bird with fly()Penguins can’t flySeparate class for non-flying birds
    Square inherits RectangleArea breaks expectationsDesign Square separately
    Subclass breaks parent’s ruleThrows, skips behaviorBehaves as parent promises

    Key Tips to Follow LSP

    • Derived classes must honor contracts/behavior of base class.
    • Don’t override methods in a way that changes expected behavior.
    • Use interfaces or abstract classes to separate capabilities (like flying).
    • Prefer composition over inheritance when behaviors differ.

    Want to Try a LSP-based Mini Project?

    We can build:

    • A PaymentMethod base class (pay())
    • Subclasses: CreditCard, UPI, Wallet
    • And see how breaking or respecting LSP affects real logic

    FAQ on Liskov Substitution Principle (LSP) in C++

    1. What is the Liskov Substitution Principle (LSP) in C++?

    The Liskov Substitution Principle (LSP) is one of the SOLID principles of object-oriented programming. It states that objects of a derived class should be replaceable with objects of their base class without affecting the correctness of the program. In simple terms, subclasses should behave like their parent classes without breaking functionality.

    2. Why is the Liskov Substitution Principle important in software design?

    LSP ensures code reliability, reusability, and maintainability. By following it, developers prevent unexpected behaviors when using inheritance. It reduces bugs, improves abstraction, and makes the system easier to extend in the future.

    3. Can you give a simple C++ example of the Liskov Substitution Principle?

    Yes.

    #include <iostream>
    using namespace std;
    
    class Bird {
    public:
        virtual void fly() { cout << "Bird can fly\n"; }
    };
    
    class Sparrow : public Bird {
    public:
        void fly() override { cout << "Sparrow flying high\n"; }
    };
    
    // Substitution works correctly
    int main() {
        Bird* b = new Sparrow();
        b->fly();  // Works fine as Sparrow behaves like Bird
        delete b;
        return 0;
    }
    

    Here, Sparrow can substitute Bird without breaking the program, which follows LSP.

    4. What happens if LSP is violated in C++?

    Violating LSP leads to runtime errors, unexpected behaviors, and poor code design. For example, if a derived class overrides a base class function in a way that changes its expected behavior, the system may produce wrong results or even crash.

    5. How does the Liskov Substitution Principle relate to C++ design patterns?

    Many C++ design patterns, such as Strategy, Template Method, and Factory Method, rely on LSP to ensure that subclasses can replace base classes seamlessly. Without LSP, these patterns lose their flexibility and correctness.

    6. How can I check if my C++ code follows the Liskov Substitution Principle?

    You can verify by asking:

    • Can I replace every instance of the base class with the subclass?
    • Does the subclass maintain the expected behavior of the parent?
      If the answer is yes, then your code follows LSP.

    7. What are real-life examples of the Liskov Substitution Principle?

    • Vehicles: A Car is a type of Vehicle and can be used wherever a Vehicle is expected.
    • Shapes: A Circle should behave like a Shape without altering the expected behavior of a Shape class.
    • Payment Systems: A CreditCardPayment should work wherever a generic PaymentMethod is required.

    8. How does LSP improve C++ code quality?

    • Promotes polymorphism
    • Reduces tight coupling
    • Makes code testable and scalable
    • Encourages clean architecture with fewer bugs

    9. What is the difference between LSP and the Open-Closed Principle (OCP)?

    • LSP ensures that subclasses can replace base classes without breaking functionality.
    • OCP ensures that classes are open for extension but closed for modification.
      Both are connected — violating LSP often means violating OCP as well.

    10. How is Liskov Substitution Principle used in modern C++ projects in 2025?

    In 2025 C++ development, LSP is applied in:

    • Embedded systems for safe hardware abstraction
    • Game development for character and object hierarchies
    • Enterprise applications for scalable and maintainable architectures
    • Design patterns to ensure flexible and reusable solutions

    You can also Visit other tutorials of Embedded Prep 

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

  • Single Responsibility Principle (SRP) | Master CPP Design Patterns 2026

    When writing clean and maintainable code, the Single Responsibility Principle (SRP) is one of the most important design principles to follow. It is the first principle in the SOLID design principles, which are a set of five rules that help developers write better object-oriented code.

    In this article, you’ll learn:

    • What the Single Responsibility Principle is
    • Why it matters
    • Real-world and code examples
    • Benefits of using SRP
    • Best practices to follow

    What is the Single Responsibility Principle?

    The Single Responsibility Principle (SRP) states:

    “A class should have only one reason to change.“

    This means that a class should do only one thing and should have only one responsibility. In other words, it should only focus on one task or job.

    If a class is doing multiple unrelated tasks, then it violates SRP and becomes harder to understand, test, and maintain.

    Real-World Analogy

    Imagine you own a house and hire a cleaner, a plumber, and an electrician. Each one has a specific responsibility:

    • The cleaner cleans the house.
    • The plumber fixes water leaks.
    • The electrician repairs electric faults.

    Now, imagine one person doing all three jobs. If anything goes wrong, it’s harder to pinpoint the issue or fix just one part without affecting the others.

    The same concept applies in programming.

    Code Example Without SRP (Bad Practice)

    class Report {
    public:
        void generateReport() {
            // Logic to generate the report
        }
    
        void printReport() {
            // Logic to print the report
        }
    
        void saveToFile() {
            // Logic to save the report to a file
        }
    };
    

    What’s wrong here?

    This class is doing too much:

    1. Creating a report
    2. Printing the report
    3. Saving the report

    These are different responsibilities, and each one might change for a different reason.

    Code Example With SRP (Good Practice)

    class ReportContent {
    public:
        void generateReport() {
            // Only generates the report content
        }
    };
    
    class ReportPrinter {
    public:
        void printReport() {
            // Only responsible for printing
        }
    };
    
    class ReportSaver {
    public:
        void saveToFile() {
            // Only responsible for saving
        }
    };
    

    Now, each class does only one job, and if anything changes, you only need to modify that specific class. This makes the code clean, testable, and easy to maintain.

    Why is SRP Important?

    • Improves readability – Each class has a clear and focused purpose.
    • Simplifies testing – You can test each class independently.
    • Reduces bugs – Changes in one class don’t affect others unexpectedly.
    • Supports reusability – Classes with a single purpose are easier to reuse.
    • Enhances maintainability – Easier to fix issues or update code later.

    How to Identify SRP Violations

    Ask yourself:

    • Does the class do more than one thing?
    • Can I split this into smaller, more focused classes?
    • Are there multiple reasons this class might change?

    If the answer is yes, then it’s time to refactor your code using SRP.

    Best Practices to Follow

    • Focus each class on one and only one responsibility.
    • Write unit tests to check if a class is tightly focused.
    • Organize files and folders based on responsibilities.
    • Keep your methods short and clear.
    • Refactor often when a class starts growing too large.

    Conclusion

    The Single Responsibility Principle is all about keeping your code clean, organized, and focused. By giving each class a single job, you make your codebase easier to understand, test, and maintain — especially as your project grows.

    You can also Visit other tutorials of Embedded Prep 

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

  • D – Dependency Inversion Principle (DIP) | Master CPP Design Patterns 2026

    Definition (Simple Terms)

    Dependency Inversion Principle : “High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.”

    Let’s Break It Down

    This sounds complex, right? Let’s simplify.

    Imagine you’re building a robot. The brain of the robot is the high-level module — it decides what to do. The legs or arms are low-level modules — they do the actual work.

    Now, if the robot’s brain is tightly connected (depends directly) to the specific type of leg motors, then anytime you change the leg motors, you also have to change the brain’s code. That’s bad.

    DIP says: “Don’t connect the brain directly to the legs. Let both talk through a common language (interface or abstract class).”

    That way, if you ever replace the legs with wheels, the brain doesn’t need to know — it just sends the same instructions.

    Goal of DIP

    • Reduce tight coupling between classes/modules.
    • Increase flexibility, maintainability, and testability of code.
    • Make it easier to swap or change parts of the system.

    Traditional (Wrong) Design Without DIP

    class Keyboard {
    public:
        void input() {
            std::cout << "Keyboard input received\n";
        }
    };
    
    class Computer {
        Keyboard keyboard;  // Direct dependency
    public:
        void getInput() {
            keyboard.input();
        }
    };
    

    What’s wrong?

    • Computer is tightly bound to Keyboard.
    • If you want to replace Keyboard with Touchscreen, you have to change the Computer class.

    DIP Applied (Right Way)

    Step 1: Create an abstraction (interface)

    class IInputDevice {
    public:
        virtual void input() = 0; // Pure virtual function
        virtual ~IInputDevice() = default;
    };
    

    Step 2: Implement the abstraction in concrete classes

    class Keyboard : public IInputDevice {
    public:
        void input() override {
            std::cout << "Keyboard input received\n";
        }
    };
    
    class Touchscreen : public IInputDevice {
    public:
        void input() override {
            std::cout << "Touchscreen input received\n";
        }
    };
    

    Step 3: Depend on the abstraction

    class Computer {
        IInputDevice* inputDevice; // Depends on abstraction
    public:
        Computer(IInputDevice* device) : inputDevice(device) {}
    
        void getInput() {
            inputDevice->input();  // Calls via interface
        }
    };
    

    Step 4: Use it flexibly

    int main() {
        Keyboard kb;
        Touchscreen ts;
    
        Computer comp1(&kb);  // Works with keyboard
        Computer comp2(&ts);  // Works with touchscreen
    
        comp1.getInput();  // Output: Keyboard input received
        comp2.getInput();  // Output: Touchscreen input received
    
        return 0;
    }
    

    Key Takeaways

    ConceptExplanation
    High-Level ModuleThe main controller (e.g., Computer)
    Low-Level ModuleThe working components (e.g., Keyboard, Touchscreen)
    AbstractionA shared interface both modules depend on (e.g., IInputDevice)
    Dependency InversionThe direction of dependency is flipped — both rely on abstraction

    Benefits of Using DIP

    • Easy to switch or add new modules (like adding a mouse or joystick).
    • Improved testability (you can inject mock devices for testing).
    • Better code organization and readability.
    • Promotes Open/Closed Principle (open to extension, closed to modification).

    Real-World Analogy

    Think of a power plug. Your phone charger (low-level device) and your home socket (high-level supply) both depend on a standard plug shape (interface). If tomorrow you buy a new charger, as long as it supports the same plug, you don’t need to rewire your home.

    DIP and Unit Testing

    Since you depend on interfaces, you can write mock versions during testing:

    class MockInputDevice : public IInputDevice {
    public:
        void input() override {
            std::cout << "Mock input for testing\n";
        }
    };
    

    Final Thoughts

    • DIP doesn’t mean no dependencies. It means depend on abstractions rather than concrete implementations.
    • It’s not about eliminating dependencies, but inverting the direction of dependency to favor abstraction.

    Full Comparison of SOLID Principles

    PrincipleFull FormCore IdeaHigh-Level PurposeReal-World AnalogyCode Example HintBenefitsViolating It Leads To
    SSingle Responsibility PrincipleA class should have only one reason to changeBreak down big classes into smaller ones, each doing one jobA chef shouldn’t also be the waiter, cashier, and cleanerSplit Invoice and InvoicePrinterMaintainable, modular codeHard to test, change, and reuse
    OOpen/Closed PrincipleSoftware entities should be open for extension, but closed for modificationAdd new functionality without changing existing codeAdding new plugins to a browser without editing its coreUse inheritance or strategy patternFlexible and extendable codeRisk of breaking existing functionality
    LLiskov Substitution PrincipleSubclasses should be replaceable for their parent classes without altering behaviorDesign classes such that any subclass can be used safely in place of its baseA square should behave like a rectangle if inherited from itAvoid incorrect inheritance like Bird -> Penguin flyingPolymorphic behavior works as expectedUnexpected behavior and bugs
    IInterface Segregation PrincipleClients shouldn’t be forced to depend on interfaces they don’t useBreak big interfaces into smaller, specific onesDon’t give a remote with 50 buttons to someone who only wants to change the volumeSplit IMultifunctionPrinter into IPrint, IScanMinimal and focused contractsConfusing, bloated interfaces
    DDependency Inversion PrincipleHigh-level modules should not depend on low-level modules, both should depend on abstractionsUse interfaces or abstract classes to decouple componentsA plug point shouldn’t care which brand charger you useInject dependencies via constructor or interfaceFlexible, testable, modular architectureTight coupling, hard to replace components

    Detailed Descriptions Per Column

    Core Idea

    • S: One job per class.
    • O: Add, don’t change.
    • L: Substitutable behavior.
    • I: Small, focused interfaces.
    • D: Depend on interfaces, not concrete classes.

    Real-World Analogy

    • Each principle is inspired by common-sense organization and responsibility. These analogies help you remember and visualize them better.

    Code Hint

    • Gives a small direction of what to implement or avoid in code for each principle.

    Benefits

    • Each principle improves maintainability, testability, and flexibility in different ways. Together, they lead to a clean and scalable architecture.

    Violations Lead To

    • Points out what goes wrong when the principle is not followed — like tight coupling, difficult changes, or confusing code.

    How They Work Together (Flow Summary)

    1. SRP gives each class one purpose.
    2. OCP lets you grow your app by adding features instead of modifying old code.
    3. LSP ensures that your new classes don’t break the old ones.
    4. ISP keeps your classes from knowing too much they don’t care about.
    5. DIP connects your parts flexibly through abstraction.

    You can also Visit other tutorials of Embedded Prep 

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

  • I – Interface Segregation Principle (ISP) | Master CPP Design Patterns 2026

    The Interface Segregation Principle (ISP) is one of the five SOLID principles of object-oriented programming. It states:

    “Clients should not be forced to depend on interfaces they do not use.“

    In simpler terms, this principle encourages splitting large, unwieldy interfaces into smaller, more specific ones so that classes only need to implement the methods they actually use.

    Why Interface Segregation Principle Matters

    When designing systems—especially in embedded software and large-scale applications—overly fat interfaces can lead to:

    • Unnecessary code coupling
    • Wasted resources in constrained systems
    • Fragile software that’s hard to maintain or extend

    By adhering to ISP, you build cleaner, modular, and more maintainable codebases that are easier to test and evolve over time.

    What does this mean (simple version)?

    Imagine an interface (abstract class) with 10 methods.
    You only need 2 of them — but you still have to override all 10, even the ones you don’t use. 😖

    That’s bad design.

    With ISP, you split that big interface into smaller purpose-specific ones so that:

    • Classes only implement what they really need
    • They’re not bloated with useless methods

    Real-Life Analogy

    Let’s say you go to a restaurant.

    If you only want coffee ☕, but they hand you a menu with 100 items, and ask you to order from every category — that’s overwhelming, right?

    Wouldn’t it be better if they had a “Drinks” menu separate from the “Meals” and “Desserts”?

    That’s Interface Segregation — keep things small, focused, and relevant.

    Bad Example: Violates ISP

    Let’s create a fat interface:

    class IMachine {
    public:
        virtual void print() = 0;
        virtual void scan() = 0;
        virtual void fax() = 0;
    };
    

    Now you want to create a basic printer:

    class OldPrinter : public IMachine {
    public:
        void print() override {
            cout << "Printing..." << endl;
        }
    
        void scan() override {
            // Not supported
            throw runtime_error("Scan not supported");
        }
    
        void fax() override {
            // Not supported
            throw runtime_error("Fax not supported");
        }
    };
    

    Problem:

    • You’re forced to implement scan() and fax() even if your class doesn’t need them.
    • You end up writing empty methods or throwing exceptions — that’s bad.

    Good Design: Follows ISP

    Split the large interface into smaller ones:

    class IPrinter {
    public:
        virtual void print() = 0;
    };
    
    class IScanner {
    public:
        virtual void scan() = 0;
    };
    
    class IFax {
    public:
        virtual void fax() = 0;
    };
    

    Now your classes only implement what they actually need:

    class BasicPrinter : public IPrinter {
    public:
        void print() override {
            cout << "Basic printing..." << endl;
        }
    };
    
    class MultiFunctionPrinter : public IPrinter, public IScanner, public IFax {
    public:
        void print() override {
            cout << "Printing..." << endl;
        }
    
        void scan() override {
            cout << "Scanning..." << endl;
        }
    
        void fax() override {
            cout << "Faxing..." << endl;
        }
    };
    

    Now:

    • BasicPrinter isn’t forced to implement scan() or fax()
    • Each class only depends on what it actually uses
    • You’re following the Interface Segregation Principle

    How to Spot ISP Violations

    SymptomIndicates ISP Violation?
    Class implements unused methods✅ Yes
    You throw exceptions in unused methods✅ Yes
    Interface has too many responsibilities✅ Yes
    Difficult to test one specific feature✅ Yes

    Tips to Apply ISP

    • 🔹 If your interface has more than 3–4 methods, check if it can be split.
    • 🔹 Group methods by behavior (e.g., Print, Scan, Fax = 3 separate concerns).
    • 🔹 Don’t overdo it — small, cohesive interfaces are ideal.
    • 🔹 Use multiple inheritance of interfaces for combining features in C++.

    ISP Mini Project Idea

    Let’s say you’re building a Vehicle system.

    Bad:

    class IVehicle {
    public:
        virtual void fly() = 0;
        virtual void drive() = 0;
        virtual void sail() = 0;
    };
    

    Now what if you just want to implement a car? 🚗

    Instead:

    class IDriveable {
    public:
        virtual void drive() = 0;
    };
    
    class IFlyable {
    public:
        virtual void fly() = 0;
    };
    
    class ISailable {
    public:
        virtual void sail() = 0;
    };
    

    Now a car only implements IDriveable, and a boat only implements ISailable.
    No more useless methods!

    Summary of ISP

    FeatureViolates ISP ❌Follows ISP ✅
    Big interfaces✅❌
    Empty/unimplemented methods✅❌
    Multiple small interfaces❌✅
    Clear class responsibilities❌✅

    Common Ground Between SRP and ISP

    They Both Want…Description
    🎯 Focused ResponsibilityBoth promote classes/interfaces doing only one thing
    🔍 Separation of ConcernsThey avoid overloaded classes or interfaces with too many duties
    💡 Clean & Maintainable CodeThey make systems modular, testable, and extendable

    Key Difference Between SRP and ISP

    PrincipleWhat it Applies ToFocusReal-World Example
    🟡 SRPClassesEach class should have one reason to changeA ReportGenerator class should only generate, not email or print
    🔵 ISPInterfacesClients should only depend on what they useA Printer class shouldn’t be forced to implement scan()

    In Summary:

    ➕ SRP:

    “Don’t overload classes with multiple jobs.”

    ➕ ISP:

    “Don’t overload interfaces with methods clients won’t use.”

    Think of it like:

    LayerSRP Deals WithISP Deals With
    Implementation“What this class is doing”“What this class is promising to do”
    Behavior Contracts—Interfaces

    Side-by-Side Quick Example

    // SRP
    class ReportGenerator {
    public:
        void generate() { /* ... */ }
    };
    // Bad: also sends emails or logs things
    
    // ISP
    class IPrinter {
    public:
        virtual void print() = 0;
    };
    
    class IScanner {
    public:
        virtual void scan() = 0;
    };
    // Good: only implement what you need
    

    Final Takeaway:

    🔧 SRP keeps classes focused.
    🔌 ISP keeps interfaces focused.

    They work hand-in-hand to make your code clean, modular, and friendly to change.

    You can also Visit other tutorials of Embedded Prep 

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

  • Adapter Design Pattern in C++ | Master CPP Design Patterns 2026

    Introduction of Adapter Design Pattern

    Adapter Design Pattern : When working on large software systems, you may come across situations where two incompatible interfaces need to work together. Instead of modifying existing code (which may be risky or impossible), you can use the Adapter Pattern to make them compatible.

    The Adapter Pattern acts like a bridge between two interfaces. It’s part of the Structural Design Patterns family in C++.

    What is the Adapter Design Pattern?

    The Adapter Design Pattern allows incompatible interfaces to work together by wrapping an existing class with a new interface.

    In simple terms, it’s like using a power plug adapter that lets a U.S. plug fit into a European socket.

    Real-life Analogy

    Imagine you have an old VGA cable that only works with projectors, but your laptop only has an HDMI port. You can’t plug it in directly. So, you use an HDMI-to-VGA adapter. Now your laptop can talk to the projector!

    That’s exactly what the Adapter Pattern does in code.

    Components of the Adapter Pattern

    1. Target – The expected interface (what your code is designed to use)
    2. Adaptee – The existing class that needs adapting
    3. Adapter – The class that wraps the Adaptee and makes it compatible with Target

    When to Use the Adapter Pattern

    • When you want to reuse an existing class, but its interface doesn’t match what you need.
    • When you want to decouple code from third-party libraries or legacy code.
    • When integrating new features into old systems.

    Adapter Pattern in C++ – Code Example

    Let’s look at a simple C++ example to understand how it works.

    Problem:

    We have an existing OldPrinter class. But our system expects printers to use the NewPrinterInterface.

    Step 1: Define the Target Interface

    // Target Interface
    class NewPrinterInterface {
    public:
        virtual void printDocument(const std::string& text) = 0;
        virtual ~NewPrinterInterface() {}
    };
    

    Step 2: Existing Class (Adaptee)

    // Adaptee
    class OldPrinter {
    public:
        void oldPrint(const std::string& text) {
            std::cout << "Old Printer Output: " << text << std::endl;
        }
    };
    

    Step 3: Create the Adapter

    // Adapter
    class PrinterAdapter : public NewPrinterInterface {
    private:
        OldPrinter* oldPrinter;
    
    public:
        PrinterAdapter(OldPrinter* printer) : oldPrinter(printer) {}
    
        void printDocument(const std::string& text) override {
            // Convert the request to match the old interface
            oldPrinter->oldPrint(text);
        }
    };
    

    Step 4: Client Code

    int main() {
        OldPrinter* legacyPrinter = new OldPrinter();
    
        // Use adapter to make it compatible with the new interface
        NewPrinterInterface* printer = new PrinterAdapter(legacyPrinter);
    
        printer->printDocument("Hello, Adapter Pattern!");
    
        delete printer;
        delete legacyPrinter;
    
        return 0;
    }
    

    Output

    Old Printer Output: Hello, Adapter Pattern!
    

    Success! The OldPrinter works with the new system using the adapter.

    Types of Adapters

    • Class Adapter (using inheritance): Inherit from both Target and Adaptee (only works in languages with multiple inheritance like C++).
    • Object Adapter (using composition): Use a reference to Adaptee inside the Adapter. This is more flexible and used in our example above.

    Benefits of Adapter Pattern

    • Promotes reusability of existing classes
    • Helps in code migration and integration
    • Makes your design more flexible and maintainable

    Drawbacks

    • Adds extra layers of code
    • May lead to performance overhead
    • Can make debugging slightly harder due to indirection

    Adapter vs. Other Patterns

    PatternPurpose
    AdapterMakes incompatible interfaces compatible
    DecoratorAdds behavior to an object dynamically
    BridgeDecouples abstraction from implementation
    FacadeProvides a simplified interface

    Conclusion

    The Adapter Pattern is a powerful tool in a C++ developer’s design toolbox. It allows your code to stay clean, extensible, and compatible with legacy or third-party systems. By wrapping old interfaces with new ones, you avoid rewriting or duplicating code.

    Use it wisely when integrating different systems — and you’ll be building robust and scalable software like a pro!

    FAQs – Adapter Pattern in C++

    Q1. Is Adapter Pattern a structural design pattern?
    Yes, it’s part of the Structural design patterns.

    Q2. Can we implement Adapter without inheritance?
    Yes! You can use composition (object adapter) for a more flexible solution.

    Q3. Is Adapter Pattern the same as a Wrapper?
    They are similar. A wrapper is a broader term; an adapter is a type of wrapper that converts interfaces.

    You can also Visit other tutorials of Embedded Prep 

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

  • Startup Code Explained: 5 Critical Things You Must Know in Embedded Systems (2026)

    Understand embedded startup code in depth from reset handlers to memory initialization with 5 critical insights engineers must know. Perfect for beginners and pros alike!

    What is Startup Code? | Beginner-Friendly Guide

    Startup code is a small but essential piece of code that runs before your main program starts. It prepares your system so that your program can run smoothly. This is especially important in embedded systems and low-level programming.

    Simple Definition :

    Startup code is the very first code that executes when a device is powered on or reset. It sets up the environment so that the main() function can run properly.

    Why is Startup Code Important?

    Think of startup code as a stage crew in a play. Before the actors come on stage, the crew sets the lighting, positions the props, and makes sure everything is ready. Similarly, startup code:

    • Sets up memory sections like .data, .bss, and stack
    • Initializes hardware components if needed
    • Calls constructors (in C++)
    • Finally, it jumps to the main() function

    What Does Startup Code Typically Do?

    1. Set the Stack Pointer
      Sets the stack to a known location in RAM.
    2. Initialize Data Section
      Copies initialized global variables from Flash to RAM.
    3. Zero out the BSS Section
      Sets all uninitialized global and static variables to zero.
    4. Call System Initialization Code
      Optional hardware-specific setup.
    5. Call main() Function
      After everything is set up, control is passed to your program’s main() function.

    Where is Startup Code Found?

    • In embedded systems, startup code is usually written in assembly or C.
    • It is provided by the compiler toolchain (like GCC, Keil, etc.) or custom-written for a specific microcontroller.

    Conclusion

    Startup code may not be visible when writing high-level programs, but it plays a crucial role behind the scenes. Especially in embedded development, understanding how startup code works can help you debug issues, customize behavior, and build better systems.

    u can also Visit other tutorials of Embedded Prep 

    Special thanks to @embedded-prep for contributing to this article on Embedded Prep