Developing in C for the ATmega328P: Better Serial Input

3 minute read

Where I discuss how to improve on the serial input of C and the ATmega328P.

Introduction

In the example serialio_string (code on GitHub or below), I demonstrate the problem with reading text from the serial port. If you use scanf(), it appears to work well, except you can easily over-run the buffer. For example, the program asks for “up to 7 char”, however, it will accept as many as you are willing to type. More than likely, after about 20 characters, the microcontroller will crash.

On the other hand, fgets() won’t allow more than the specified number of characters, however, it doesn’t perform well when less than the number of characters are entered. It requires multiple carriage returns (or enters), until it has filled its buffer. And when printing the buffer back out, problems occur as well, such as over-writing previous lines.

readLine() - the solution

I created a new Library function called readLine(uint8_t *buffer, uint8_t buffersize) to enable easy reading from the serial port of the ATmega328P (in this case, Uno). As shown the function will take two parameters, *buffer, which points to an array for which to store the text input and buffersize, which advises the maximum size of the array. It will read upto buffersize number of characters or until EOL, in this case, is ASCII CR or decimal 13 character.

readline() - example

// serialio - demonstrate how to successfully read a line of text and
// use STRTOK() to split the line into tokens (or words)

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

#define MAX_BUFFER 16
#define MAX_TOKENS (MAX_BUFFER/2 + 1)
#define MAX_DELIMS 3
int main(void) {

    init_serial();
    char input[MAX_BUFFER + 1] = {};
    char delims[MAX_DELIMS + 1] = {" ,\t"};

    puts("Serial I/O Test: readLine with tokens");
    printf("Enter text up to %i characters, and end w/ CR\n", MAX_BUFFER);
    printf("Line will be parsed into tokens\n");
    uint8_t num_char = readLine(input, MAX_BUFFER);

    printf("You entered %i characters\n", num_char);

    for (uint8_t out_char=0; out_char<MAX_BUFFER; out_char++)
    {
        printf("%c", input[out_char]);
    }
    printf("\n");

Break the line into tokens

It is rare that you would want to use the entire line at once. More than likely, you will want to break the line into tokens (or words), then use each word for a particular purpose. An easy way to do this is to use strtok(). Here’s [excellent tutorial on how to use strtok(). Or you can examine the code segment below. It follows the code above, once the line is read, it is tokenized:

    // break input line into tokens
    char *tokens[MAX_TOKENS];
    uint8_t token = 0;
    tokens[token] = strtok(input, delims);
    while ((tokens[token] != NULL) && (token < MAX_TOKENS)) {
        token++;
        tokens[token] = strtok(NULL, delims);
    }
    uint8_t tokens_found = token;

    printf("Found %i tokens, delimitors were (w/ ASCII code): ", tokens_found);
    for (uint8_t delim=0; delim < MAX_DELIMS; delim++)
    {
        printf("'%c' 0x%x ", delims[delim], delims[delim]);
    }
    printf("\n");
    for (token=0; token<tokens_found; token++)
    {
        printf("%i %s\n", token, tokens[token]);

    }

This is a very interesting approach and needs to be understood to use properly. In the code above, tokens[] will end up as an array of pointers to each token found in the string. The delimitor, is replaced by a NULL, which creates the individual words (or tokens). Please read Majenko’s article, if you wish to understand this concept.

serialio_string

This code demonstrates the issues with using scanf() or fgets() on reading input. Note what happens when using the two functions:

  • scanf() - allows a buffer over-run crashing the microcontroller
  • fgets() - expects a specific number of characters, losing flexibility in serial input
// Demonstrate the problems with scanf() and fgets() to read a string
// from the serial console
// scanf() will allow a buffer over-run, resulting in a crash
// fgets() only allows a set number of char, limiting flexibility and overwrites
// once it has read the alloted number of char

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

int main(void) {    

    init_serial();
    #define MAX_CHAR 8
    char input1[MAX_CHAR] = {};
    char input2[MAX_CHAR] = {};

    puts("Serial I/O Test: scanf() and fgets()");
    while(1)
    {
        printf("scanf(): Enter up to %i char\n", MAX_CHAR - 1);
        scanf("%s", input1);
        printf("You entered %s\n", input1);

        printf("fgets(): Enter %i char\n", MAX_CHAR - 1);
        fgets(input2, MAX_CHAR, stdin);
        printf("\nYou entered %s\n", input2);
    }
}

Comments powered by Talkyard.