As usual, include files go first. While conventionally, system header files (those in angular brackets < ... >) go before application-specific header files (in double quotes), defines.h comes as the first header file here. The main reason is that this file defines the value of F_CPU which needs to be known before including <utils/delay.h>.

The function ioinit() summarizes all hardware initialization tasks. As this function is declared to be module-internal only (static), the compiler will notice its simplicity, and with a reasonable optimization level in effect, it will inline that function. That needs to be kept in mind when debugging, because the inlining might cause the debugger to "jump around wildly" at a first glance when single-stepping.

The definitions of uart_str and lcd_str set up two stdio streams. The initialization is done using the FDEV_SETUP_STREAM() initializer template macro, so a static object can be constructed that can be used for IO purposes. This initializer macro takes three arguments, two function macros to connect the corresponding output and input functions, respectively, the third one describes the intent of the stream (read, write, or both). Those functions that are not required by the specified intent (like the input function for lcd_str which is specified to only perform output operations) can be given as NULL.

The stream uart_str corresponds to input and output operations performed over the RS-232 connection to a terminal (e.g. from/to a PC running a terminal program), while the lcd_str stream provides a method to display character data on the LCD text display.

The function delay_1s() suspends program execution for approximately one second. This is done using the _delay_ms() function from <util/delay.h> which in turn needs the F_CPU macro in order to adjust the cycle counts. As the _delay_ms() function has a limited range of allowable argument values (depending on F_CPU), a value of 10 ms has been chosen as the base delay which would be safe for CPU frequencies of up to about 26 MHz. This function is then called 100 times to accomodate for the actual one-second delay.

In a practical application, long delays like this one were better be handled by a hardware timer, so the main CPU would be free for other tasks while waiting, or could be put on sleep.

At the beginning of main(), after initializing the peripheral devices, the default stdio streams stdin, stdout, and stderr are set up by using the existing static FILE stream objects. While this is not mandatory, the availability of stdin and stdout allows to use the shorthand functions (e.g. printf() instead of fprintf()), and stderr can mnemonically be referred to when sending out diagnostic messages.

Just for demonstration purposes, stdin and stdout are connected to a stream that will perform UART IO, while stderr is arranged to output its data to the LCD text display.

Finally, a main loop follows that accepts simple "commands" entered via the RS-232 connection, and performs a few simple actions based on the commands.

First, a prompt is sent out using printf_P() (which takes a program space string). The string is read into an internal buffer as one line of input, using fgets(). While it would be also possible to use gets() (which implicitly reads from stdin), gets() has no control that the user's input does not overflow the input buffer provided so it should never be used at all.

If fgets() fails to read anything, the main loop is left. Of course, normally the main loop of a microcontroller application is supposed to never finish, but again, for demonstrational purposes, this explains the error handling of stdio. fgets() will return NULL in case of an input error or end-of-file condition on input. Both these conditions are in the domain of the function that is used to establish the stream, uart_putchar() in this case. In short, this function returns EOF in case of a serial line "break" condition (extended start condition) has been recognized on the serial line. Common PC terminal programs allow to assert this condition as some kind of out-of-band signalling on an RS-232 connection.

When leaving the main loop, a goodbye message is sent to standard error output (i.e. to the LCD), followed by three dots in one-second spacing, followed by a sequence that will clear the LCD. Finally, main() will be terminated, and the library will add an infinite loop, so only a CPU reset will be able to restart the application.

There are three "commands" recognized, each determined by the first letter of the line entered (converted to lower case):

  • The 'q' (quit) command has the same effect of leaving the main loop.

  • The 'l' (LCD) command takes its second argument, and sends it to the LCD.

  • The 'u' (UART) command takes its second argument, and sends it back to the UART connection.

Command recognition is done using sscanf() where the first format in the format string just skips over the command itself (as the assignment suppression modifier * is given).