Search code examples
cinterruptbit-shiftatmelstudioatmega32

ATmega32: Bitshifting in the Code changes the Interrupt Overflow Duration


In our C Code we are Bitshifting a 1:

    if(posizion_LED_MODUL == 3) //This statement is True, and this LED is Switched correctly
    {
        PORTC |= (1 << led);
    }

This somehow messes with the Overflow Interrupt that is inverting a LED on PORTC:

/***INTERUPT*/
ISR(TIMER2_OVF_vect)
{
    PORTC ^= 0x02;
    TCNT2 = 0;
}

This seems to not Switch the LED (Not the same as the one above) at a regular intervall.

I Uploadet the whole Code here for Reference: https://pastebin.com/X1GhvVXc

In the Process of finding out what the Problem could be, we tried replacing this:

int LED_MODUL_An(uint8_t led)
{
    if(posizion_LED_MODUL == 1)
    {
        PORTA |= (1 << led);
    }
    
    if(posizion_LED_MODUL == 2)
    {
        PORTB |= (1 << led);
    }
    if(posizion_LED_MODUL == 3)
    {
        PORTC |= (1 << led);
    }
    if(posizion_LED_MODUL == 4)
    {
        PORTD |= (1 << led);
    }
    return 0;
}

int LED_MODUL_Aus(uint8_t led)
{
    if(posizion_LED_MODUL == 1)
    {
        PORTA &= ~(1 << led);
    }
    if(posizion_LED_MODUL == 2)
    {
        PORTB &= ~(1 << led);
    }
    if(posizion_LED_MODUL == 3)
    {
        PORTC &= ~(1 << led);
    }
    if(posizion_LED_MODUL == 4)
    {
        PORTD &= ~(1 << led);
    }
    return 0;
}

With this:

int LED_MODUL_An(uint8_t led)
{
    if(posizion_LED_MODUL == 1)
    {
        PORTA |= 1;
    }
    
    if(posizion_LED_MODUL == 2)
    {
        PORTB |= 1;
    }
    if(posizion_LED_MODUL == 3)
    {
        PORTC |= 1;
    }
    if(posizion_LED_MODUL == 4)
    {
        PORTD |= 1;
    }
    return 0;
}

int LED_MODUL_Aus(uint8_t led)
{
    if(posizion_LED_MODUL == 1)
    {
        PORTA &= 0xFE;
    }
    if(posizion_LED_MODUL == 2)
    {
        PORTB &= 0xFE;
    }
    if(posizion_LED_MODUL == 3)
    {
        PORTC &= 0xFE;
    }
    if(posizion_LED_MODUL == 4)
    {
        PORTD &= 0xFE;
    }
    return 0;
}

This fixed the irregular blinking and the LED now blinked at regular intervalls. But we don't know why a bit shift in another Function would disturb the Interrupt.

We are using Microchip Studio with the built in compiler.

The Fuses are set like this:
Image of the Fuses Tab

If any of you have suggestions to fix the Problem, we would be very thankfull.


Solution

  • The problem is the variable shift amount in the first case. The compiler needs to calculate the pattern to reset and to set, respectively, the desired bit. Because this results in a variable value, the compiler cannot generate an atomic instruction. Instead it produces code to read the port register, to modify the value with the variable pattern, and finally to write the result back to the port register.

    In contrast, if you use a constant shift amount, the compiler is able to generate a single instruction, because it knows the bit to reset and to set, respectively.

    See this minimal example:

    #include <avr/io.h>
    #include <stdint.h>
    
    void reset_with_variable(uint8_t led)
    {
        PORTA &= ~(1 << led);
    }
    
    void set_with_variable(uint8_t led)
    {
        PORTA |= 1 << led;
    }
    
    void reset_with_constant(void)
    {
        PORTA &= ~1;
    }
    
    void set_with_constant(void)
    {
        PORTA |= 1;
    }
    

    The compilation with optimization results in the following assembly code. I have marked the accesses to the port register.

    __SP_H__ = 0x3e
    __SP_L__ = 0x3d
    __SREG__ = 0x3f
    __tmp_reg__ = 0
    __zero_reg__ = 1
    reset_with_variable(unsigned char):
    .L__stack_usage = 0
            in r25,0x1b     ; <- READ
            ldi r18,lo8(1)
            ldi r19,0
            rjmp 2f
            1:
            lsl r18
            2:
            dec r24
            brpl 1b
            com r18
            and r18,r25     ; <- MODIFY
            out 0x1b,r18    ; <- WRITE
            ret
    set_with_variable(unsigned char):
    .L__stack_usage = 0
            in r25,0x1b     ; <- READ
            ldi r18,lo8(1)
            ldi r19,0
            rjmp 2f
            1:
            lsl r18
            2:
            dec r24
            brpl 1b
            or r25,r18      ; <- MODIFY
            out 0x1b,r25    ; <- WRITE
            ret
    reset_with_constant():
    .L__stack_usage = 0
            cbi 0x1b,0      ; <- READ-MODIFY-WRITE
            ret
    set_with_constant():
    .L__stack_usage = 0
            sbi 0x1b,0      ; <- READ-MODIFY-WRITE
            ret
    

    It is worse than I expected, because the compiler puts all the pattern calculation code between the read instruction and the write instruction, widening the interval between both.

    Anyway, what we commonly expect is an atomic read-modify-write. But the CPU does not provide such an instruction for variable patterns.

    Now the error happens: During the interval calculating the pattern to modify the read value, an interrupt occurs. Its service routine modifies the port register, as expected, and correctly. But after the interrupt service routine returns to the interrupted sequence, r25 still has the previous value of the port register. This one is modified by the pattern and written back to the port register. The modification by the interrupt service routine is reverted.

    The longer the interval, the higher the probability for the error to happen.

    In the case with constant shift amounts the problem does not exist, because the compiler generates a single instruction, which cannot be interrupted and is therefore atomic.


    Now, how do you solve the issue?

    You need to see the single C statement as a critical section, which is executed by multiple assembly instructions.

    One possible solution is to disable the interrupts during the C statement. This will postpone the possible execution of the interrupt service until the section is left.

    #include <avr/interrupt.h>
    #include <avr/io.h>
    #include <stdint.h>
    
    void reset_with_variable(uint8_t led)
    {
        cli();
        PORTA &= ~(1 << led);
        sei();
    }
    
    void set_with_variable(uint8_t led)
    {
        cli();
        PORTA |= (1 << led);
        sei();
    }
    

    Lesson to learn: If you encounter such "strange" problems, look into the generated assembly code. You will need to learn to read and understand assembly at least a bit, but it will raise your understanding very much.