Developing in C for the ATmega328P: Pointers

11 minute read

Where I describe how to develop use pointers in AVR_C.

Introduction

Pointers are introduced by “K&R” with the following comment “Pointers have been lumped with the goto statement as a marvelous way to create impossible- to-understand programs.” While pointers are simple in nature, they can become quite complex, quickly. I recommend going through the examples on this page, slowly and methodically as well as making changes and determine if the change had the effects you believed it would. I also recommend having a C Language reference manual open as well, as I won’t got through the usage.

Overview

Pointers contain the address of the element they point to such as a variable, a structure, a string or a function. The concept of pointing to the address of the element as compared to the value of the element, is called indirection. As a fundamental concept in computing, it enables complex concepts to be created easily and usually more clearly. It eases the use of linked lists, polymorphism and function overloading.

I’ll walk through a series of examples in how to use pointers. At the end of this entry, is the complete file to use to work with the examples. Examine the printf statements in each example, then change some of the values either in the initialization stage or in the print statement to further understand how pointers work and are constructed.

Simple Pointers

Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    // Simple Pointers
    int16_t x = 1234;
    int16_t y = 5678;

    int16_t *p_x;
    int16_t *p_y;
    int16_t *p_z;

    x++;
    y--;

    p_x = &x;
    p_y = &y;
    p_z = NULL;

    puts("Simple Pointers");
    printf("The address of x is %p and its value is %i\n", p_x, x);        
    printf("The address of y is %p and its value is %i\n", p_y, y);
    printf("The address of z is %p and its NULL\n", p_z);
    if (!p_z) 
    {
        printf("The address of z is %p and it is NULL\n", p_z);
    }

Output

Simple Pointers
The address of x is 0x8eb and its value is 1235
The address of y is 0x8ed and its value is 5677
The address of z is 0 and its NULL

Discussion

This section shows pointers in their most simple form. There are two variables, x and y each with a pointer p_x and p_y, respectively. The print statements show the address and value of each variable. There is also a variable z, which has neither a value nor an address. Note that Z has an address of 0 and it has been set to NULL.

Nothing significant here, other than to understand the usage of * and the & as well as initializing pointers to a address or NULL. Note the conditional as it pertains to p_z, C allows you to easily test for NULL by if (p) (not equal to NULL) or if (!p) (equal to NULL).

Pointers to pointers

Code

1
2
3
4
5
6
7
8
    // Pointers to pointers
    int16_t **p_p_x;
    p_p_x = &p_x;

    puts("\nPointers to pointers");
    printf("The address of the pointer to x is %p\n", p_p_x);
    printf("Addr: %p contains Addr: %p which contains: %i\n", p_p_x, p_x, x);        
    printf("      p_p_x   points to     p_x which contains     x\n");        

Output

Pointers to pointers
The address of the pointer to x is 0x8ef
Addr: 0x8ef contains Addr: 0x8eb which contains: 1235
      p_p_x   points to     p_x which contains     x

Discussion

This section shows pointers with an additional step of indirection, pointers to pointers. It is important to understand that adding an additional * allows that pointer to point to a pointer. An example of this indirection would be in linked-lists, where the a pointer will point to the next pointer in the list. In this example, we have a pointer which points to the pointer of x.

Again, by itself, the code doesn’t have a significant impact, its simply important to understand the concept of pointers of pointers.

Pointers and Arrays

Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    // Pointers and Arrays
    uint8_t numbers[] = {0, 1, 2, 3, 4};
    uint8_t *p_numbers;
    numbers[3] = 33;
    p_numbers = numbers;

    puts("\nPointers and Arrays");
    printf("The address of numbers is %p, it contains %u == 0th value is %u\n",\
        p_numbers, *p_numbers, numbers[0]);
    for (uint8_t i = 0; i< sizeof(numbers)/sizeof(numbers[0]); i++)
    {        
        printf("Address %u of numbers is %p and %p and the values are %u and %u\n",\
            i, p_numbers, &numbers[i], *p_numbers, numbers[i]);
        p_numbers++;    
    }
    p_numbers = numbers;
    printf("The value of p_numbers[3] is %u\n", p_numbers[3]);

Output

Pointers and Arrays
The address of numbers is 0x8f1, it contains 0 == 0th value is 0
Address 0 of numbers is 0x8f1 and 0x8f1 and the values are 0 and 0
Address 1 of numbers is 0x8f2 and 0x8f2 and the values are 1 and 1
Address 2 of numbers is 0x8f3 and 0x8f3 and the values are 2 and 2
Address 3 of numbers is 0x8f4 and 0x8f4 and the values are 33 and 33
Address 4 of numbers is 0x8f5 and 0x8f5 and the values are 4 and 4
The value of p_numbers[3] is 33

Discussion

Pointers and arrays are tightly linked, and in many situations, you could use a pointer as easily as an array reference. For example, note that in line 8 the use of the pointer *p_numbers is the same as referencing the 0th element of the array. Incrementing the pointer, allows referencing the next element. This is why it is also important to understand that a pointer has a data type. Each data type has a size in memory and the pointer will increment (or decrement) by element size, not by address byte.

Using a pointer for an array becomes important when wanting to pass an array to a function. If doing so by value, the entire array would need to be duplicated in memory while passing by reference allows the existing array to be used with a significant savings in memory.

Pointers and Structures

Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
    // Pointers and Structures
    typedef struct LED 
    {
       volatile uint8_t pin;    // Uno pin 
       uint8_t state;           // Is pin HIGH or LOW
       uint16_t on;             // Time on
       uint16_t off;            // Time off
       uint16_t elapsed;        // Time elapsed sinced last in loop
    } LED;

    struct LED *p_LED;
    struct LED LED0;
    LED0.pin = 13;
    LED0.state = 1;
    LED0.on = 1000;
    LED0.off = 500;
    LED0.elapsed = 237;
    p_LED = &LED0;

    puts("\nPointers and Structures");
    printf("LED0 %p contains pin %u state %u on %u off %u and elapsed %u\n",\
            p_LED, (*p_LED).pin, (*p_LED).state, p_LED->on, p_LED->off, p_LED->elapsed);

Output

Pointers and Arrays
LED0 0x8f6 contains pin 13 state 1 on 1000 off 500 and elapsed 237

Discussion

Pointers and structures also work well together. Using a pointer to a structure is extremely common (if not mandatory), when passing a struct to a function. Much like the similarity between pointers and arrays, there is a congruity to pointers and structures in such a way, the common usage is pointer_to_struct->member_of_struct, which is a more common usage than the expected dot notation of (*pointer_to_struct).member_of_struct. You see both forms of notation in Line 22 above.

Pointers and Functions

Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int16_t addition(int16_t a, int16_t b) { return a + b; }
int16_t subtraction(int16_t a, int16_t b) { return a - b; }

   // Pointers and Functions
    int16_t (*p_func)(int16_t a, int16_t b);

    puts("\nPointers and Functions");
    p_func = addition;
    int16_t results = (*p_func)(x, y);
    printf("p_func points to addition at address %p with %i + %i = %i\n",\
        p_func, x, y, results);
    p_func = subtraction;
    results = (*p_func)(x, y);
    printf("p_func points to subtraction at address %p with %i - %i = %i\n",\
        p_func, x, y, results);

Output

Pointers and Functions
p_func points to addition at address 0x5f with 1235 + 5677 = 6912
p_func points to subtraction at address 0x62 with 1235 - 5677 = -4442

Discussion

Pointers and functions are a powerful combination. Using a pointer to determine which function to run enables polymorphism, multi-tasking or function over-loading. Its best to spend some time understanding the specific steps to make this happen:

  1. Setup functions and function pointer Lines 1-2 and 5
  2. Set the function_pointer to the address of the desired function Line 8 or 12
  3. Call the function with desired arguments Line 9 or 13

Structure with Pointer Element

A different situation from a struct pointer, is a pointer to a struct, is a struct with a pointer as an element. In this situation, it takes a little bit of work to determine how to manipulate the pointer.

In my situation, I had a pointer element (volatile uint8_t *port;) which contained the address of PORTD or PORTB (known collectively as PORTn) on the ATmega328P. I also needed to use the address of the data direction register, DDRD or DDRB, respectively. The address of this register is 1 less than that of the address of the PORTn register.

Seems easy enough, set the value of a variable equal to PORTn then decrement the variable and we’re good to go.

The solution was this code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
typedef struct servo {
   uint8_t bit;               // ATmega328P pin bit
   volatile uint8_t *port;    // ATmega328P port for pin, PORTD or PORTB
   uint8_t state;             // is pulse HIGH or LOW
   uint16_t high_width;       // pulse time on
   uint16_t low_width;        // pulse            // pulse time off
   uint16_t high_count;       // used by ISR to track n high counts
   uint16_t low_count;        // used by ISR to track n low counts
} servo;

void set_servo(uint8_t index, uint8_t bit, volatile uint8_t *port, uint8_t state,\
    uint16_t high_width)
{
    servos[index].bit = bit;
    DDRn = port;
    servos[index].port = port;
    servos[index].state = state;
    servos[index].high_width = high_width;
    servos[index].low_width = SERVO_PULSE_WIDTH - high_width;
    servos[index].high_count = high_width;
    servos[index].low_count = servos[index].low_width;

    // DDRn is 1 addr below PORTn, set bit in DDRn to enable pin to OUTPUT
    DDRn--;
    set_bit(*DDRn, servos[index].bit);
    return ;
}

A print example

Using similar logic to the example immediately above, I wanted to have two simple macros which would convert an Uno pin to an ATmega328P PORTn and Bitn. I determined using two macros was more efficient than trying to create a function which would return the two variables. In order to test the macros pintoPort() and pintoBit(), I created an example examples/pintoPort:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Simple conversion program to show Uno pin to ATmega328P port address and bit 
#include <stdio.h>
#include "uart.h"
#include "unolib.h"

int main(void) {    

    init_serial();
    puts("Testing pintoBit and pintoPort, enter an Uno Pin number (0-13)");
    
    while(1) 
    {
        uint8_t Unopin;
        volatile uint8_t *port;
        scanf("%hhu",&Unopin);
        uint8_t bit = pintoBit(Unopin);
        port = pintoPort(Unopin);
        printf("Pin %d Port %p Bit %u\n", Unopin, port, bit);        
    }
        
    return 0;
    # Output: Pin 13 Port 0x25 Bit 5
}

I found I needed to define the port pointer separately (line 14), get its value using pintoPort() on line 17 then print the pointer as a pointer using %p in the printf() statement on line 18. It’s was helpful (if not critical) to keep the declaration and the initialization on two different lines.

Entire File of Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
// pointers - explore using pointers

#include <stdio.h>
#include "uart.h"

int16_t addition(int16_t a, int16_t b) ;
int16_t subtraction(int16_t a, int16_t b) ;

int main(void) {    

    init_serial();
    
    // Simple Pointers
    int16_t x = 1234;
    int16_t y = 5678;
    int16_t *p_x;
    int16_t *p_y;
    int16_t *p_z;

    x++;
    y--;

    p_x = &x;
    p_y = &y;
    p_z = NULL;

    // Pointers to pointers
    int16_t **p_p_x;
    p_p_x = &p_x;

    // Pointers and Arrays
    uint8_t numbers[] = {0, 1, 2, 3, 4};
    uint8_t *p_numbers;
    numbers[3] = 33;
    p_numbers = numbers;

    // Pointers and Structures
    typedef struct LED 
    {
       volatile uint8_t pin;    // Uno pin 
       uint8_t state;           // Is pin HIGH or LOW
       uint16_t on;             // Time on
       uint16_t off;            // Time off
       uint16_t elapsed;        // Time elapsed sinced last in loop
    } LED;

    struct LED *p_LED;
    struct LED LED0;
    LED0.pin = 13;
    LED0.state = 1;
    LED0.on = 1000;
    LED0.off = 500;
    LED0.elapsed = 237;
    p_LED = &LED0;

    // Pointers and Functions
    int16_t (*p_func)(int16_t a, int16_t b);

    // Output
    puts("Simple Pointers");
    printf("The address of x is %p and its value is %i\n", p_x, x);        
    printf("The address of y is %p and its value is %i\n", p_y, y);
    printf("The address of z is %p and its NULL\n", p_z);
    
    puts("\nPointers to pointers");
    printf("The address of the pointer to x is %p\n", p_p_x);
    printf("Addr: %p contains Addr: %p which contains: %i\n", p_p_x, p_x, x);        
    printf("      p_p_x   points to     p_x which contains     x\n");        
    
    puts("\nPointers and Arrays");
    printf("The address of numbers is %p, it contains %u == 0th value is %u\n",\
        p_numbers, *p_numbers, numbers[0]);
    for (uint8_t i = 0; i< sizeof(numbers)/sizeof(numbers[0]); i++)
    {        
        p_numbers = &numbers[i];    
        printf("Address %u of numbers is %p and the value is %u\n",\
            i, p_numbers, numbers[i]);
    }
    
    puts("\nPointers and Structures");
    printf("LED0 %p contains pin %u state %u on %u off %u and elapsed %u\n",\
            p_LED, p_LED->pin, p_LED->state, p_LED->on, p_LED->off, p_LED->elapsed);

    puts("\nPointers and Functions");
    p_func = addition;
    int16_t results = (*p_func)(x, y);
    printf("p_func points to addition at address %p with %i + %i = %i\n",\
        p_func, x, y, results);
    p_func = subtraction;
    results = (*p_func)(x, y);
    printf("p_func points to subtraction at address %p with %i - %i = %i\n",\
        p_func, x, y, results);

    return 0;
}

int16_t addition(int16_t a, int16_t b) 
{
    return a + b;
}

int16_t subtraction(int16_t a, int16_t b) 
{
    return a - b;
}

Comments powered by Talkyard.