Skip to main content
Engineering LibreTexts

18.3: Controller Specific Stuff

  • Page ID
    35884
  • Moving on to other header files, we must recall that there are dozens and dozens of models in a given processor series like the AVR. Each of these controllers will have different memory capacities, IO capabilities and so forth, so we need to distinguish which one we’re using while also trying to keep the code as generic as possible. Normally, this is done by creating specific header files for each controller. The IDE then gives you an option to select which controller you’re using and #defines a controller ID for it (see the list in the Arduino IDE under Tools>>Board). Consider the following chunk of avr/io.h:

    #ifndef _AVR_IO_H_
    #define _AVR_IO_H_
    
    #include <avr/sfr_defs.h>
    

    We find a (huge) series of conditional includes, each looking for the one pre-defined processor symbol set by the Arduino IDE:

    #if defined (__AVR_AT94K__)
    #    include <avr/ioat94k.h>
    #elif defined (__AVR_AT43USB320__)
    #    include <avr/io43u32x.h>
    #elif defined (__AVR_AT43USB355__)
    #    include <avr/io43u35x.h>
    

    ... and so on until we get to the ATmega 328P for the Arduino Uno:

    #elif defined (__AVR_ATmega328P__)
    #    include <avr/iom328p.h> 
    

    ... and we continue until we get to the end:

    #elif defined (__AVR_ATxmega256A3B__)
    #    include <avr/iox256a3b.h>
    #else
    #    if !defined(__COMPILING_AVR_LIBC__)
    #        warning "device type not defined"
    #    endif
    #endif
    
    #include <avr/portpins.h>
    #include <avr/common.h>
    #include <avr/version.h>
    
    #endif /* _AVR_IO_H_ */
    

    So what’s in avr/iom328p.h you ask? This includes a bunch of things that will make our programming lives much easier such as definitions for ports, registers and bits. We’re going to be seeing these over and over:

    #ifndef _AVR_IOM328P_H_
    #define _AVR_IOM328P_H_ 1
    /* Registers and associated bit numbers */
    

    This is an 8 bit input port and associated bits

    #define PINB _SFR_IO8(0x03)
    #define PINB0 0
    #define PINB1 1
    #define PINB2 2
    #define PINB3 3
    #define PINB4 4
    #define PINB5 5
    #define PINB6 6
    #define PINB7 7
    

    This is an 8 bit data direction register and associated bits

    #define DDRB _SFR_IO8(0x04)
    #define DDB0 0
    #define DDB1 1
    #define DDB2 2
    #define DDB3 3
    #define DDB4 4
    #define DDB5 5
    #define DDB6 6
    #define DDB7 7
    

    This is an 8 bit output port and associated bits

    #define PORTB _SFR_IO8(0x05)
    #define PORTB0 0
    #define PORTB1 1
    #define PORTB2 2
    #define PORTB3 3
    #define PORTB4 4
    #define PORTB5 5
    #define PORTB6 6
    #define PORTB7 7
    

    ...and so on for ports C and D. Now for analog to digital converter (ADC) goodies:

    #ifndef __ASSEMBLER__
    #define ADC     _SFR_MEM16(0x78)
    #endif
    #define ADCW    _SFR_MEM16(0x78)
    
    #define ADCL _SFR_MEM8(0x78)
    #define ADCL0 0
    #define ADCL1 1
    #define ADCL2 2
    #define ADCL3 3
    #define ADCL4 4
    #define ADCL5 5
    #define ADCL6 6
    #define ADCL7 7
    
    #define ADCH _SFR_MEM8(0x79)
    #define ADCH0 0
    #define ADCH1 1
    #define ADCH2 2
    #define ADCH3 3
    #define ADCH4 4
    #define ADCH5 5
    #define ADCH6 6
    #define ADCH7 7
    
    #define ADCSRA _SFR_MEM8(0x7A)
    #define ADPS0 0
    #define ADPS1 1
    #define ADPS2 2
    #define ADIE 3
    #define ADIF 4
    #define ADATE 5
    #define ADSC 6
    #define ADEN 7
    
    #define ADCSRB _SFR_MEM8(0x7B)
    #define ADTS0 0
    #define ADTS1 1
    #define ADTS2 2
    
    #define ACME 6
    
    #define ADMUX _SFR_MEM8(0x7C)
    #define MUX0 0
    #define MUX1 1
    #define MUX2 2
    #define MUX3 3
    #define ADLAR 5
    #define REFS0 6
    #define REFS1 7
    

    ... and so on for the remainder of the header file. OK, so just what is this nugget below?

    #define PORTB _SFR_IO8(0x05)
    

    Most controllers communicate via memory-mapped IO. That is, external pins are written to and read from as if they are ordinary memory locations. So if you want to write to a port, you can simply declare a pointer variable, set its value to the appropriate address, and then manipulate it as needed. For the ATmega 328P, the address of IO Port B is 0x25. You could write the following if you wanted to set bit 5 high:

    unsigned char *portB;
    
    portB = (unsigned char *)0x25; // cast required to keep compiler quiet
    *portB = *portB | 0x20;
    

    You could also use the bit setting function seen earlier which is a little clearer:

    bitSet( *portB, 5 );
    

    The problem here is that you have to declare and use the pointer variable which is a little clunky. There’s also the error-prone business of pointer de-referencing in the assignment statement (the * in front of the variable which beginning programmers tend to forget). Besides, it requires that you look up the specific address of the port (and change it if you use another processor). So to make things more generic we place the addresses (or more typically, offsets from a base address) in the processor-specific header file.

    The header file declares an item, PORTB, for our convenience. It is defined as _SFR_IO8(0x05) but what’s that _SFR_IO8 function? In sfr_defs.h it’s defined as follows:

    #define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)
    

    and __SFR_OFFSET is defined as 0x20. In other words, this item is 0x05 above the base offset address of 0x20, meaning it’s the address 0x25 as we saw earlier. But what the heck is _MMIO_BYTE()? That looks a little weird at first glance:

    #define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))
    

    This is just a cast with a pointer de-reference. It says this item is a pointer to an unsigned char which is being de-referenced (de-referenced by the first * and don’t forget the earlier typedef for the uint8_t). By placing all of these items in header files we’ve managed to make the IO programming generic while at the same time removing the need for pointer variable declarations and the need for pointer de-referencing.1

    Therefore, if we want to set bit 5 high, we can now just say

    PORTB = PORTB | 0x20; // or more typically: PORTB |= 0x20;
    

    or

    bitSet( PORTB, 5 ); // note lack of “*” in both lines of code
    

    All the business about exactly where PORTB is located in the memory map is hidden from us and there will be no accidental errors due to leaving out the *. The same code will work with several different processors, and to top it off, it’s operationally more efficient than dealing with pointers as well. Consequently, we will normally use these symbolic register and port names rather than hard addresses in future work. Simply lovely. Time for a snack; something fruity perhaps, something that reminds us of tropical islands...

     


    1. If you're wondering about volatile, recall that it's a modifier that indicates that the variable can be changed by another process (possibly an interrupt). This will prevent overly aggressive assembly code optimizations by the compiler.