Setting pin to high in function instead of main, not full voltage output?

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!


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>