Developing in C for the ATmega328P: Better Serial Input

5 minute read

Where I discuss how to improve on the serial input of C and the ATmega328P and adding a second serial port, soft_serial.

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.

I had forgotten how to use strtok() and was mistakenly attempting to copy each token into a char array using strncpy(). Doh! The good way to use the end result tokens[], is to continue to use tokens[] as the pointer and add an enum to identify each token, like this:

enum {cmd, first, second, third};
execute(tokens[cmd]); // call function execute with token[0] acting as a command
// remaining tokens in token[] can act as parameters, first, second... 

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);
    }
}

soft_serial

While developing the code for the xArm_Commander, I needed a second serial port on the Uno. I created a very simple, slow, serial port called soft_serial.

The example uses both serial ports, the Uno UART and soft_serial connected to pins 2 (RX) and 3 (TX). I use a Adafruit Blue USB Type A to 4 Wire serial cable to connect to soft_serial, using the Green (TX) pin to connect to Un RX pin 2 and the White (RX) pin to connect to Un TX pin 3. (Yes, you cross TX to RX and RX to TX.)

The file examples/soft_serial/main.c will demonstrate how to use soft_serial. *You will want to run make complete to ensure you have all of the changes for soft_serial.

In the terminal, I using tio to to the UART:

tio -b 250000 /dev/tty.usbmodem3101
[06:56:19.429] tio v3.3
[06:56:19.430] Press ctrl-t q to quit
[06:56:19.432] Connected to /dev/tty.usbmodem3101
Soft serial has been initialized and title printed
On soft term 9 chars received: abcdefghi

The UART may continue to run at the BAUD rate specified in env.make.

And I use CoolTerm to connect to soft_serial:

Enter up to 9 chars: 
9 chars received
abcdefghi

The soft_serial baud rate and serial pins are set in Library/soft_serial.h:

#define SOFT_RX_PIN PIND2 // Define the RX pin
#define SOFT_TX_PIN PIND3 // Define the TX pin
#define SOFT_BAUD 9600

The fastest I have been able to reliably run soft_serial is 28800. I typically run it at 9600.

Comments powered by Talkyard.