6

I have a function which turns on a LED by setting PB1 to HIGH. The light from the LED is barely visible. If I set PB1 to HIGH in my main function the light from the LED is as bright as it should be.

It makes no sense to me since it's just changing a value, which is either 0 or 1. I must be missing something extremely obvious, but what could it be?

Here's some background information:

  • The LED is connected in series with a resistor so it's not being shorted
  • I have tried the two different codes attached multiple times to make sure the problem is reproduced every time
  • I have tried to use a different pin to rule out that there is something wrong with that specific pin

Edit: Added some more information and tags

  • Chip: ATmega328-PU
  • Programmer: AVRisp MKII

Here's the code with the pin set in main:

#include <avr/io.h>

int main(void) {    
    DDRB |= (1 << PB1);

    PORTB |= (1 << PB1); 

    while(1) {
    }

    return 0;
}

And here's the code where the pin is set in a function:

#include <avr/io.h>

void turn_on_led();

int main(void) {
    DDRB |= (1 << PB1);

    turn_on_led();

    while(1) {
    }

    return 0;
}

void turn_on_led()
{
    PORTB |= (1 << PB1); 
}

Here is the makefile:

main: 
    avr-gcc -g -Os -Wall -mmcu=atmega328 -c ../src/example.c

elf:
    avr-gcc example.o -o example.elf    

hex: 
    avr-objcopy -O ihex example.elf example.hex 

dump: 
    avr-objdump -h -S example.o > example.lst   

upload:
    avrdude -p m328 -c avrispmkII -P usb -U flash:w:example.hex

clean:
    rm -f *.o
    rm -f *.hex
    rm -f *.lst

Fuse setting:

avrdude: Device signature = 0x1e9514
avrdude: safemode: lfuse reads as E2
avrdude: safemode: hfuse reads as D9
avrdude: safemode: efuse reads as 7

Fuse setting input in a fuse calculator to show individual bits:

Fuse calculator

Disassembly of setting pin in main:

example.o:     file format elf32-avr

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000006  00000000  00000000  00000034  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  00000000  00000000  0000003a  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  00000000  00000000  0000003a  2**0
                  ALLOC
  3 .debug_abbrev 0000004e  00000000  00000000  0000003a  2**0
                  CONTENTS, READONLY, DEBUGGING
  4 .debug_info   00000082  00000000  00000000  00000088  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING
  5 .debug_line   000000c2  00000000  00000000  0000010a  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING
  6 .debug_frame  00000020  00000000  00000000  000001cc  2**2
                  CONTENTS, RELOC, READONLY, DEBUGGING
  7 .debug_pubnames 0000001b  00000000  00000000  000001ec  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING
  8 .debug_pubtypes 0000001e  00000000  00000000  00000207  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING
  9 .debug_aranges 00000020  00000000  00000000  00000225  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING
 10 .debug_str    000000d9  00000000  00000000  00000245  2**0
                  CONTENTS, READONLY, DEBUGGING

Disassembly of section .text:

00000000 <main>:
#include <avr/io.h>

int main(void) {
    DDRB |= (1 << PB0);
   0:   20 9a           sbi 0x04, 0 ; 4

    PORTB |= (1 << PB0); 
   2:   28 9a           sbi 0x05, 0 ; 5
   4:   00 c0           rjmp    .+0         ; 0x6 <__zero_reg__+0x5>

Disassembly of setting pin in function:

example.o:     file format elf32-avr

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         0000000c  00000000  00000000  00000034  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  00000000  00000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  00000000  00000000  00000040  2**0
                  ALLOC
  3 .debug_abbrev 00000061  00000000  00000000  00000040  2**0
                  CONTENTS, READONLY, DEBUGGING
  4 .debug_info   00000096  00000000  00000000  000000a1  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING
  5 .debug_line   000000dc  00000000  00000000  00000137  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING
  6 .debug_frame  00000030  00000000  00000000  00000214  2**2
                  CONTENTS, RELOC, READONLY, DEBUGGING
  7 .debug_pubnames 0000002b  00000000  00000000  00000244  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING
  8 .debug_pubtypes 0000001e  00000000  00000000  0000026f  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING
  9 .debug_aranges 00000020  00000000  00000000  0000028d  2**0
                  CONTENTS, RELOC, READONLY, DEBUGGING
 10 .debug_str    000000e5  00000000  00000000  000002ad  2**0
                  CONTENTS, READONLY, DEBUGGING

Disassembly of section .text:

00000000 <turn_on_led>:
    return 0;
}

void turn_on_led()
{
    PORTB |= (1 << PB1); 
   0:   29 9a           sbi 0x05, 1 ; 5
}
   2:   08 95           ret

00000004 <main>:
#include <avr/io.h>

void turn_on_led();

int main(void) {
    DDRB |= (1 << PB1);
   4:   21 9a           sbi 0x04, 1 ; 4

    turn_on_led();
   6:   0e 94 00 00     call    0   ; 0x0 <turn_on_led>
   a:   00 c0           rjmp    .+0         ; 0xc <main+0x8>
rzetterberg
  • 403
  • 4
  • 12
  • 1
    Just a side note: you should use `#define`s to rename the LED outputs and avoid 'magic numbers', as it results in a cleaner and more readable code. – clabacchio Feb 29 '12 at 12:11
  • Yes, or using a enum for multiple related numbers. The idea is to give numbers a constant name so that: 1) When you want to change the number there are a central place where you can change it, 2) You have a verbose description of what the number represent, instead of just the value which isn't very descriptive. – rzetterberg Feb 29 '12 at 12:38
  • What are your fuse settings? Is the watchdog enabled? If you avr-objdump both versions, what's the difference? Does the version with the function work if the function is inline'd? – Toby Jaffey Feb 29 '12 at 12:40
  • @Joby Taffey: I added the fuse settings and dumps in the question. The only thing I've changed is to disable CKDIV8. – rzetterberg Feb 29 '12 at 12:49
  • 4
    I have zero experience with AVR coding itself, just Arduino experience. With Arduino, the default pin mode is INPUT, which sets the pins to high impedance - LEDs *can* light up (writing a HIGH enables a pullup resistor), but very faintly. When switching them to OUTPUT mode, the pins can source a lot more current. Could something related to this be the issue? Honestly I don't see how by reading the code, but I thought I'd mention it as it bit me when I was new to the Arduino environment. – exscape Feb 29 '12 at 12:55
  • @rzetterberg for fun you should put DDRB |= (1 << PB1); before PORTB |= (1 << PB1); in the turn_led_on function and see if it behaves the same (without the static keyword) – vicatcu Feb 29 '12 at 16:02
  • @exscape Actually, you are right. If I exclude the instruction setting the port to output, the behavior is exactly the same. Good catch :) – rzetterberg Mar 01 '12 at 10:27
  • @vicatcu I tried to put them both in the function without static keyword and it works fine. It's like setting the DDRB is lost when it's not just above setting PORTB. – rzetterberg Mar 01 '12 at 10:27

3 Answers3

9

Wow, that's pretty crazy. Those programs are almost identical. Just for easier comparison, the first program, with the assignment in main, has the assembly (with comments):

0:   20 9a           sbi 0x04, 0 ; Set Bit of IO for DDRB
2:   28 9a           sbi 0x05, 0 ; Set Bit of IO for PORTB
4:   00 c0           rjmp    .+0 ; Relative JuMP of 0 bytes functions as `while(1);`

The second program, with the assignment in a function, has the assembly:

0:   28 9a           sbi 0x05, 0 ; Set Bit of IO for PORTB
2:   08 95           ret         ; Return to the address on the call stack 
4:   20 9a           sbi 0x04, 0 ; Set Bit of IO for DDRB
6:   0e 94 00 00     call      0 ; Call the function at address 0 (at top)
a:   00 c0           rjmp    .+0 ; Relative JuMP of 0 bytes functions as `while(1);`

The third program does an interesting optimization - it removes the function call completely, inlining it and making this program identical to the first. You could probably get an identical effect with inline void turn_on_led(). For completeness' sake, the assembly is:

0:   20 9a           sbi 0x04, 0 ; Set Bit of IO for DDRB
2:   28 9a           sbi 0x05, 0 ; Set Bit of IO for PORTB
4:   00 c0           rjmp    .+0 ; Relative JuMP of 0 bytes functions as `while(1);`

The Interrupt Vector Table

The addresses 0 to a are addresses within the .text section, not in the program memory. The .text section starts at offset 0x34, per the File off[set] directive:

Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000006  00000000  00000000  00000034  2**0

What's really at addresses 0-34 is (usually) the Interrupt Vector Table. If you had selected the BOOTRST flag, it would be at the start of the bootloader, but you didn't so it's not. The first element in the Interrupt Vector Table tells the processor where to go on reset or boot-up.

This location should be the first instruction of main for these programs (0 for the first case, 4 for the second case), but it's possible that it's defaulting to address 0 in the second program, which would set the output high while the data direction register was still set to input per the defaults. This would turn on the weak pullup, and the change of the data direction register later on would have no effect.

I'd guess that this is what's happening. To test whether your issue is that your output is only using the weak pullup, you could remove the DDRB assignment entirely from either program. The result should be a dimly-lit LED. If it's not the same brightness, then this isn't your problem. If it is the same brightness, I'd guess that this is, in fact, your problem.

Alternative explanations, not likely to be the problem (but could be in other situations)

Do you need a delay?

Another hiccup could be the mention in section 11.2 of the datasheet, "Ports as General Digital I/O" that:

As shown in Figure 11-2, the PINxn Register bit and the preceding latch constitute a synchronizer. This is needed to avoid metastability if the physical pin changes value near the edge of the internal clock, but it also introduces a delay. Figure 11-3 shows a timing diagram of the synchronization when reading an externally applied pin value.

Therefore, in every example of reading a pin, there is a null operation during which this synchronizer delay is accounted for. Use __no_operation(); in C for this effect. This need for a delay is very common in embedded systems programming; it's cheaper to make the programmer stick a delay in his code than it is to make some things happen in a single clock cycle.

In your first program, you have no such delay. In your second program, you have a delay. This should cause the first program not to work, but the second program should work. This isn't what's happening, and there's no such delay on the outputs, so I doubt that this is the problem.

Accidental PWM

Your assembly demonstrates that this is not the case, but a common mistake is to forget the while(1). This can cause the processor to go into reset immediately after turning the LED on. While the processor is resetting itself and the DDRB register is being set, the LED is off. Then, it's briefly on, and the reset starts all over again. This forms a rudimentary PWM system on accident, causing the LED to appear dim.

However, you do have a while(1) (note that for(;;) is a popular equivalent), and it does appear in the assembly as rjmp .+0, so this doesn't seem to be your problem. I am a little confused by the 0, rjmp changes the program counter to PC + k + 1. Usually, we use labels for this when writing in assembly, and this should therefore output a k of -1, but it seems reasonable to trust that the compiler is doing the right thing here.

However, let's take a better look at the encoding. The hex code for the instruction is 00 c0. According to the AVR Instruction Set manual, the opcode for rjmp is 1100 kkkk kkkk kkkk, or, in hex, 0xCK KK, where the concatenation of K is k, our relative jump. The AVR we're using is little-endian, so 00 C0 as seen in the program is a relative jump (C) to a position 0 bytes away.

According to the operation description, this will perform the operation PC <- PC + 0 + 1, or advance the program counter beyond this address. However, it may be that this isn't the correct interpretation of this operation, I've always used labels when working with this instruction, so the actual number used has been abstracted away by the assembler.

Accusing the compiler of misinterpreting the lines

while(1) {
} 

is rather extreme, though. I don't think this is the problem.

Hope this helps!

vicatcu
  • 22,499
  • 13
  • 79
  • 155
Kevin Vermeer
  • 19,989
  • 8
  • 57
  • 102
  • Thank you for taking the time to explain the different possibilities! I do some research about what you brought up and tomorrow I will test the different scenarios to pin point the problem :) – rzetterberg Feb 29 '12 at 17:29
  • 1
    I tried removing setting PB1 to output, and it behaves exactly the same. So somehow DDRB is never set in my first program and the pin is actually acting in input mode with the pull up resistor on :S I sent a mail to avr-ggc-list hoping to get some for information about why the compiler would do this. Available here: http://lists.gnu.org/archive/html/avr-gcc-list/2012-03/msg00000.html – rzetterberg Mar 01 '12 at 10:50
5

After reading up on the information Kevin provided in his answer I read a document called "AVR32006 : Getting started with GCC for AVR32". In this document there was a particular section which got me thinking about what Kevin said about the vector table.

The linking process needs information about code and data memory location. Using a linker script provides this. If specifying which device you are compiling code for by using the ?-mpart=? option in avr32-gcc, a default linker script for that device will be used.

So I tried adding the mmcu flag to the linking command, so that it was changed from:

elf:
        avr-gcc example.o -o example.elf

To:

elf:
        avr-gcc -mmcu=atmega328 example.o -o example.elf

This proved to be the problem, the vector table was not setup properly and a lot of other parts were missing.

Here is the disassembly after adding the mmcu flag:

example.elf:     file format elf32-avr

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000090  00000000  00000000  00000054  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .stab         000006cc  00000000  00000000  000000e4  2**2
                  CONTENTS, READONLY, DEBUGGING
  2 .stabstr      00000081  00000000  00000000  000007b0  2**0
                  CONTENTS, READONLY, DEBUGGING

Disassembly of section .text:

00000000 <__vectors>:
   0:   0c 94 34 00     jmp     0x68    ; 0x68 <__ctors_end>
   4:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
   8:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
   c:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  10:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  14:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  18:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  1c:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  20:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  24:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  28:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  2c:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  30:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  34:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  38:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  3c:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  40:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  44:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  48:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  4c:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  50:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  54:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  58:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  5c:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  60:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>
  64:   0c 94 3e 00     jmp     0x7c    ; 0x7c <__bad_interrupt>

00000068 <__ctors_end>:
  68:   11 24           eor     r1, r1
  6a:   1f be           out     0x3f, r1        ; 63
  6c:   cf ef           ldi     r28, 0xFF       ; 255
  6e:   d8 e0           ldi     r29, 0x08       ; 8
  70:   de bf           out     0x3e, r29       ; 62
  72:   cd bf           out     0x3d, r28       ; 61
  74:   0e 94 42 00     call    0x84    ; 0x84 <main>
  78:   0c 94 46 00     jmp     0x8c    ; 0x8c <_exit>

0000007c <__bad_interrupt>:
  7c:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>

00000080 <turn_on_pb>:
        return 0;
}

void turn_on_pb(void)
{
        PORTB |= (1 << PB0);
  80:   28 9a           sbi     0x05, 0 ; 5
}
  82:   08 95           ret

00000084 <main>:

void turn_on_pb(void);

int main(void)
{
        DDRB |= (1 << PB0);
  84:   20 9a           sbi     0x04, 0 ; 4
        turn_on_pb();
  86:   0e 94 40 00     call    0x80    ; 0x80 <turn_on_pb>
  8a:   ff cf           rjmp    .-2             ; 0x8a <main+0x6>

0000008c <_exit>:
  8c:   f8 94           cli

0000008e <__stop_program>:
  8e:   ff cf           rjmp    .-2             ; 0x8e <__stop_program>
rzetterberg
  • 403
  • 4
  • 12
  • I wondered what that switch did. Thanks for the complete followup. +1 – JRobert Mar 02 '12 at 22:32
  • 1
    I found this: "It is important to specify the MCU type when linking. The compiler uses the -mmcu option to choose start-up files and run-time libraries that get linked together. If this option isn't specified, the compiler defaults to the 8515 processor environment, which is most certainly what you didn't want." here: http://www.nongnu.org/avr-libc/user-manual/group__demo__project.html – rzetterberg Mar 03 '12 at 09:37
0

I imagine that your issue is that the stack is not correctly configured, so the call instruction fails when it tries the save the return address. This probably results in a processor reset, resulting in accidental PWM as described by Kevin, except that it's DDRB which is affected by the PWM and PORTB is never set at all.

Ben Voigt
  • 2,743
  • 1
  • 22
  • 34