Developing in C for the ATmega328P: User Functions

7 minute read

Where I describe how to develop user functions in AVR_C.

Introduction

While much of the content in “Developing in C for the ATmega328P” is focused on the Library functions, developing your own functions is an important aspect of programming in C. Functions help you structure your code into smaller, less complex modules. This allows you to debug each module, then as you develop your code and you will feel more confident that the program will work, if all modules are error-free.

In this introduction to functions, we’ll assume the function will be in the same file as the function main. We will also assume a simple function which only requires passing parameters by value, as compared to passing parameters by reference.

Function Example

In C, functions must be declared and defined. Declaration of a function is identifying the data type of the function as well as the function’s arguments. Definition of a function is adding the statements required for the function to properly perform. In simple functions as the one below, we’ll declare and define the function, simultaneously.

Let’s imagine a situation where we have ordered a analog sensor, however, we want to continue to code before the part arrives. This simple example replaces the analogRead() function, and provides random 10 bit data much like we saw in the analogRead() Fun example.

1
2
3
4
uint16_t r_analogRead(uint8_t pin) 
{
   return (uint16_t) rand() % 1023; 
}

Details of the function:

  • Line 1: uint16_t indicates the function will return an unsigned 16 bit integer (10-bits of data)
  • Line 1: r_analogRead is the name of the function, “r” stands for random.
  • Line 1: (uint8_t pin) for consistency, the function requires a pin number as an argument, however, in this case it is not used
  • Line 2 & 4: brackets to indicate the function statements
  • Line 3: which is simply a return statement
  • Line 3: a cast of (uint16_t) is required to ensure expression returns uint16_t, as rand() returns a int16_t
  • Line 3: rand() is a psuedo-random number generator, which returns a random number between 0 - 37767
  • Line 3: % 1023% modelo 1023, ensures the function returns a number between 0 - 1023 or a 10-bit value, just like analogRead()

Function Structure

From Kernighan and Ritchie, The C Programming Language pg 70, each function has the form:

return-type function-name (argument declarations)
{
   declarations and statements
}

And K&R goes on to say “Various parts might be absent; a minimal function is

dummy() {}

Which isn’t quite correct for us. We specify both C99 as our standard for C (best for embedded development) and we ask all warnings to be treated as errors (again, best for embedded development). In our case, a minimal function is

void dummy() {}"

Why is this important? Many times, in development, you want to sketch out your program and ensure you have all of your critical calls setup. You might not have all of the code developed, so having a “stub” function is helpful.

Note: Adding (void) for the parameter list if there are no parameters, is also a best practice. We’ll do that going forward.

For example, here is the complete program using r_analogRead(), which also needs a fictional pinMode() in order to be a complete example which might be easily replaced by the Library function calls when the part arrives.:

 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
#include <stdio.h>
#include <stdlib.h>
#include "uart.h"
#include "delay.h"
#include "unolib.h"

uint16_t d_analogRead(uint8_t pin) 
{
   return (uint16_t) rand() % 1023; 
}

void d_pinMode(void) { }    // add pin and INPUT, when Library pinMode() is used

int main (void)
{
    init_serial();

    const uint8_t analog_pin = A0;
    d_pinMode();        // add pin and INPUT, when Library pinMode() is used

    puts("Testing dummy analogRead");
    while(1) {
            uint16_t analog_value = d_analogRead(analog_pin);
            float voltage = (analog_value * .00488);
            printf("Pin: %u Value: %u Voltage: %5.3f\n",\
            analog_pin, analog_value, voltage);
            delay(1000);
    }
    return 0;
}

void Functions

The r_pinMode() function above which doesn’t return a value, is a void function. There are many uses this type of function, particularly, in embedded programming. Most functions which run on your personal computer will always return a value to indicate success or failure. This makes sense, as your personal computer is interactive and error messages are not only helpful, they are easily seen. With embedded programming, a user terminal rarely exists, so error messages tend not to be very helpful and will consume valuable, rare resources.

In our case, many of our setup or I/O functions such as pinMode(pin, type), digitalWrite(pin, value),* or analogWrite(pin, value) are void functions in that they don’t return a value. Writing void functions in embedded programming is a good idea, however, be sure you are well aware how to determine if the function has failed.

Functions without Arguments

Again, the r_pinMode() function doesn’t have arguments either. As we said, this is because we want the function to serve as a stub. There are also Library functions which don’t have arguments such as the ones which initialize timers, init_sysclock_1() and init_sysclock_2(). You might write functions without parameters, however, in C, while it is correct to have a function written as void function(). It is considered a best practice to write void function(void), as this will ensure the compiler checks for incorrect calls to the function. To reduce execution errors, we want the compiler to check as much as possible, during compilation, as we don’t have an easy method of seeing our errors on a embedded processor.

A More Complex Function Example

While the dummy version of analogRead(), d_analogRead() provides random values, there are situations where we want to be more precise with the values provided. For these situations, a version of analogRead() which accepts input from the serial port might be of value. The usual method of reading from the serial port is to use scanf() so in our case, we could use scanf("%4d", &value); then reference value as our equivalent ADC value. This doesn’t solve a couple of problems:

  • Values greater than 1023
  • Values less than 0
  • Values which are non-numeric

Which means a better method would be to develop a function which performs the scanf("%4d", &value);, however, it also performs the checks necessary to faithfully reproduce an analogRead().

scanf()

The best way to understand scanf() as it pertains to the AVR family of microcontrollers is in the User Manual entry for vfscanf(). As the manual states “Formatted input. This function [vfscanf] is the heart of the scanf family of functions.”

A key points from the page which we need for our serialRead() program are these:

  • “Most conversions skip leading white space before starting the actual conversion.”

  • “Conversions are introduced with the character %. Possible options can follow the %:… d Matches an optionally signed decimal integer; the next pointer must be a pointer to int.”

  • “These functions return the number of input items assigned, which can be fewer than provided for, or even zero, in the event of a matching failure. Zero indicates that, while there was input available, no conversions were assigned; typically this is due to an invalid input character, such as an alphabetic character for a d conversion.”

Based on the points, above, we want to specify a %i format, point to an integer pointer and check for the number of values returned. If the number is zero, we want to indicate a conversion error.

analogRead()

As we want to emulate analogRead() which is a function which reads an ADC and returns a 10-bit value, we also want to ensure our values range from 0 - 1023. Values outside of this range will be set to the nearest limit. And clearly, negative values will not be returned.

One artifact I noticed when writing and testing the function, is that despite checking if there was a conversion error, the calling program would go into an infinite loop. To fix that, I added an exit(1) if a conversion error or EOF occured. See lines 11-15 (conversion error) and lines 16-20 (EOF).

serialRead()

Here is the function, written for greatest legibility. Some optimizations could be added and when this function is added to the Library, it will be split into a .h file and a .c file.

 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
#include <stdio.h>
#include <stdlib.h>
#include "uart.h"
#include "unolib.h"

uint16_t serialRead() 
{
    uint16_t value;
    uint8_t values = scanf("%4d", &value);

    if (values == 0)
    {
        puts("Conversion error, exiting.");
        exit(1);
    }
    else if (values == EOF) 
    {
        puts("EOF, exiting.");
        exit(1);
    }
    else 
    {
        if (value > 1023)
        {
            value = 1023;
        }
        if (value < 0)
        {
            value = 0;
        }
    }    return value; 
}

The program which uses the serialRead() function is this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int main (void)
{
    init_serial();

    const uint8_t analog_pin = A0;
    // d_pinMode(); // add pin and INPUT, when Library pinMode() is used

    puts("Testing serialRead, exits on non-numeric input");
    while(1) 
    {
        int16_t analog_value;
        analog_value = serialRead();
        float voltage = (analog_value * .00488);
        printf("Pin: %u Value: %u Voltage: %5.3f\n",\
        analog_pin, analog_value, voltage);
        delay(1000);
    }
    return 0;
}

Comments powered by Talkyard.