stdiodemo(3avr)
π NAME π
stdiodemo β using the standard IO facilities (that's section 3avr of the AVR libc manual, bestie π
)
πΈ SYNOPSIS πΈ
β¨ Detailed Description β¨
okay so this whole project is literally a glow-up tutorial for how to use the standard IO facilities (aka stdio) that this library hands you. it assumes you already kinda know how stdio works in a normal C app, and it hyperfixates on the stuff that's different in this library's implementation β which mostly comes down to the fact that you're on a microcontroller, not a whole hosted computer environment. it's giving "small chip, big dreams" π¦
this demo is meant to supplement the docs, not replace them. so like, don't skip the actual documentation, bestie, this is the vibe check, not the whole exam π
π Hardware setup π
the demo is set up so it can run on the ATmega16 that ships with the STK500 development kit. the UART port needs to be connected to the RS-232 "spare" port with a jumper cable that connects PD0 to RxD and PD1 to TxD. the RS-232 channel is set up as standard input (stdin) and standard output (stdout), respectively. periodt π«
and because we need a different device for a standard error channel (stderr), an industry-standard LCD display with an HD44780-compatible LCD controller was chosen. this display needs to be connected to port A of the STK500 like this:
- port
A0, header1, functionLCD D4 - port
A1, header2, functionLCD D5 - port
A2, header3, functionLCD D6 - port
A3, header4, functionLCD D7 - port
A4, header5, functionLCD R/~W - port
A5, header6, functionLCD E - port
A6, header7, functionLCD RS - port
A7, header8, functionunused - port
GND, header9, functionGND - port
VCC, header10, functionVcc
the LCD controller is used in 4-bit mode, and that includes polling the "busy" flag, so the R/~W line from the LCD controller needs to be connected, no skipping π₯
also note: the LCD controller has yet another supply pin that's used to adjust the LCD's contrast (V5). typically that pin connects to a potentiometer betweenVccandGND. often it might just work to connect that pin toGND, but leaving it unconnected usually yields an unreadable display. so likeβ¦ don't do that π
port A was chosen because 7 pins are needed to connect the LCD, and yet every other port is already partially in use: port B has the pins for in-system programming (ISP), port C has the ports for JTAG (which can be used for debugging), and port D is used for the UART connection. so port A said "fine, I'll do it myself" π
π Functional overview π
the project consists of these files:
stdiodemo.cβ this is the main example file.defines.hβ contains some global defines, like the LCD wiringhd44780.cβ implementation of an HD44780 LCD display driverhd44780.hβ interface declarations for the HD44780 driverlcd.cβ implementation of LCD character IO on top of the HD44780 driverlcd.hβ interface declarations for the LCD driveruart.cβ implementation of a character IO driver for the internal UARTuart.hβ interface declarations for the UART driver
π A code walkthrough π
π stdiodemo.c
as usual, include files go first. and yeah, conventionally system header files (the ones in angular brackets like < ... >) go before application-specific header files (the ones in double quotes), but defines.h comes as the first header file here. the main reason? this file defines the value of F_CPU, which needs to be known before including <utils/delay.h>. no cap π§
the function ioinit() summarizes all the hardware initialization tasks. because 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. keep that in mind when debugging, because the inlining might cause the debugger to "jump around wildly" at a first glance when single-stepping. it's not haunted, it's just inlined π
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, and the third one describes the intent of the stream (read, write, or both). any functions that aren't 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. simple as β¨
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. two streams, two jobs, both slaying π
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. since 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. that function is then called 100 times to accomodate for the actual one-second delay. doing the math so you don't have to π«Ά
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. main character energy = letting the hardware do the waiting π΄
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, having stdin and stdout available allows you to use the shorthand functions (e.g. printf() instead of fprintf()), and stderr can mnemonically be referred to when sending out diagnostic messages. it's giving convenience πΈ
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. yes, your error messages literally go to a tiny screen, and honestly? iconic.
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 also be 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. like, never never. not once. π₯
the details: iffgets()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 ofstdio.fgets()will returnNULLin 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 returnsEOFin 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. dramatic exit, honestly πβ¨
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). sneaky but correct π
π¦ defines.h
this file just contains a few peripheral definitions.
the F_CPU macro defines the CPU clock frequency, to be used in delay loops, as well as in the UART baud rate calculation.
the macro UART_BAUD defines the RS-232 baud rate. depending on the actual CPU frequency, only a limited range of baud rates can be supported. so, vibe check your clock speed first π
the remaining macros customize the IO port and pins used for the HD44780 LCD driver. each definition consists of a letter naming the port this pin is attached to, and a respective bit number. for accessing the data lines, only the first data line gets its own macro (line D4 on the HD44780 β lines D0 through D3 are not used in 4-bit mode), and all other data lines are expected to be in ascending order next to D4. so like, they're a little squad standing in a line π«Ά
β hd44780.h
this file describes the public interface of the low-level LCD driver that interfaces to the HD44780 LCD controller. public functions are available to:
- initialize the controller into 4-bit mode
- wait for the controller's busy bit to be clear
- read or write one byte from or to the controller
since there are two different forms of controller IO β one to send a command or receive the controller status (RS signal clear), and one to send or receive data to/from the controller's SRAM (RS asserted) β macros are provided that build on the mentioned function primitives.
finally, macros are provided for all the controller commands to allow them to be used symbolically. the HD44780 datasheet explains these basic functions of the controller in more detail. yes, the datasheet is long, but it's the blueprint, bestie π
π« hd44780.c
this is the implementation of the low-level HD44780 LCD controller driver.
on top, a few preprocessor glueing tricks are used to establish symbolic access to the hardware port pins the LCD controller is attached to, based on the application's definitions made in defines.h. preprocessor arts and crafts π¨
the hd44780_pulse_e() function asserts a short pulse to the controller's E (enable) pin. since reading back the data asserted by the LCD controller needs to be performed while E is active, this function reads and returns the input data if the parameter readback is true. when called with a compile-time constant parameter that is false, the compiler will completely eliminate the unused readback operation, as well as the return value, as part of its optimizations. the compiler is out here doing decluttering for you π
as the controller is used in 4-bit interface mode, all byte IO to/from the controller needs to be handled as two nibble IOs. the functions hd44780_outnibble() and hd44780_innibble() implement this. they do not belong to the public interface, so they are declared static. private functions, private era π
building upon these, the public functions hd44780_outbyte() and hd44780_inbyte() transfer one byte to/from the controller.
the function hd44780_wait_ready() waits for the controller to become ready, by continuously polling the controller's status (which is read by performing a byte read with the RS signal cleard), and examining the BUSY flag within the status byte. this function needs to be called before performing any controller IO. patience is a virtue and also a while loop β³
Finally, hd44780_init() is the one that boots the LCD controller into 4-bit mode, following the initialization sequence the datasheet mandates. And because the BUSY flag can't even be checked yet at this point, this is literally the only part of this code where timed delays show up. The controller can do a power-on reset when certain constraints on the power supply rise time are met, but always calling the software initialization routine at startup means you know the controller is in a known state. This function also flips the interface into 4-bit mode β and that would not happen automatically after a power-on reset. So yeah, it matters. π
lcd.h
This one declares the public interface of the higher-level (character IO) LCD driver. Simple. Clean. Iconic. π
lcd.c
The implementation of the higher-level LCD driver lives here. It builds on top of the HD44780 low-level LCD controller driver and offers a character IO interface that's suitable for direct use by the standard IO facilities. Where the low-level HD44780 driver is all about setting up controller SRAM addresses, writing data to the controller's SRAM, and controlling display functions like clearing the display or moving the cursor, this high-level driver lets you just write a character to the LCD, in the assumption this will somehow show up on the display. Lowkey magical. β¨
Control characters can be handled at this level, and they can trigger specific actions on the LCD. Currently, there's only one control character being dealt with: a newline character (\n) is taken as an indication to clear the display and set the cursor into its initial position upon reception of the next character, so a "new line" of text can be displayed. Therefore, a received newline character is remembered until more characters have been sent by the application, and will only then cause the display to be cleared before continuing. It's a convenient abstraction where full lines of text can be sent to the driver, and they stay visible at the LCD until the next line is to be displayed. π
Further control characters could be implemented, e. g. using a set of escape sequences. That way, it would be possible to implement self-scrolling display lines etc. The door is open, bestie. πͺβ¨
The public function lcd_init() first calls the initialization entry point of the lower-level HD44780 driver, and then sets up the LCD in a way we'd like to: display cleared, non-blinking cursor enabled, SRAM addresses are increasing so characters will be written left to right. π
The public function lcd_putchar() takes arguments that make it suitable for being passed as a put() function pointer to the stdio stream initialization functions and macros (fdevopen(), FDEV_SETUP_STREAM() etc.). So it takes two arguments: the character to display itself, and a reference to the underlying stream object. It is expected to return 0 upon success. No cap. π
This function remembers the last unprocessed newline character seen in the function-local static variable nl_seen. If a newline character is encountered, it simply sets this variable to a true value, and returns to the caller. As soon as the first non-newline character is to be displayed with nl_seen still true, the LCD controller is told to clear the display, put the cursor home, and restart at SRAM address 0. All other characters are sent to the display. Periodt. π
The single static function-internal variable nl_seen works for this purpose. But if multiple LCDs should be controlled using the same set of driver functions, that would not work anymore, because a way is needed to distinguish between the various displays. This is where the second parameter can be used: the reference to the stream itself. Instead of keeping the state inside a private variable of the function, it can be kept inside a private object that is attached to the stream itself. A reference to that private object can be attached to the stream (e. g. inside the function lcd_init() that then also needs to be passed a reference to the stream) using fdev_set_udata(), and can be accessed inside lcd_putchar() using fdev_get_udata(). Big brain energy. π§ β¨
uart.h
Public interface definition for the RS-232 UART driver, much like in lcd.h except there is now also a character input function available. Glow up. π¦
As the RS-232 input is line-buffered in this example, the macro RX_BUFSIZE determines the size of that buffer. Remember that number, bestie. π’
uart.c
This implements an stdio-compatible RS-232 driver using an AVR's standard UART (or USART in asynchronous operation mode). Both character output and character input operations are implemented. Character output takes care of converting the internal newline \n into its external representation carriage return/line feed (\r\n). That's the translation. π
Character input is organized as a line-buffered operation that allows you to minimally edit the current line until it is "sent" to the application when either a carriage return (\r) or newline (\n) character is received from the terminal. The line editing functions implemented are:
\b(back space) or\177(delete) deletes the previous character^u(control-U, ASCII NAK) deletes the entire input buffer^w(control-W, ASCII ETB) deletes the previous input word, delimited by white space^r(control-R, ASCII DC2) sends a\r, then reprints the buffer (refresh)\t(tabulator) will be replaced by a single space
The function uart_init() takes care of all hardware initialization that is required to put the UART into a mode with 8 data bits, no parity, one stop bit (commonly referred to as 8N1) at the baud rate configured in defines.h. At low CPU clock frequencies, the U2X bit in the UART is set, reducing the oversampling from 16x to 8x, which allows for a 9600 Bd rate to be achieved with tolerable error using the default 1 MHz RC oscillator. π₯
The public function uart_putchar() again has suitable arguments for direct use by the stdio stream interface. It performs the \n into \r\n translation by recursively calling itself when it sees a \n character. Just for demonstration purposes, the \a (audible bell, ASCII BEL) character is implemented by sending a string to stderr, so it will be displayed on the LCD. Cute, right? ππ
The public function uart_getchar() implements the line editor. If there are characters available in the line buffer (variable rxp is not NULL), the next character will be returned from the buffer without any UART interaction. No hardware drama. π
If there are no characters inside the line buffer, the input loop will be entered. Characters will be read from the UART, and processed accordingly. If the UART signalled a framing error (FE bit set), typically caused by the terminal sending a line break condition (start condition held much longer than one character period), the function will return an end-of-file condition using _FDEV_EOF. If there was a data overrun condition on input (DOR bit set), an error condition will be returned as _FDEV_ERR. So watch those error flags, bestie. π¨
Line editing characters are handled inside the loop, potentially modifying the buffer status. If characters are attempted to be entered beyond the size of the line buffer, their reception is refused, and a \a character is sent to the terminal. If a \r or \n character is seen, the variable rxp (receive pointer) is set to the beginning of the buffer, the loop is left, and the first character of the buffer will be returned to the application. (If no other characters have been entered, this will just be the newline character, and the buffer is marked as being exhausted immediately again.) And that's the whole line editor, no cap. πβ¨
The source code
The source code is the source code, bestie. You know what it is. This section is just here for the vibes. π
Author
Generated automatically by Doxygen for AVR-LibC from the source code. Robots really said "I got this." π€π
And that's the page! Source/version: AVR-LibC 2.2.1; date: the one in the footer; page: stdiodemo(3avr). Bye bestie! πβ¨π¦π
π¦ where this page comes from
- ubuntu resolute: avr-libc 1:2.2.1-1build1 Β· BSD-3-Clause
- ubuntu stonking: avr-libc 1:2.2.1-1build1 Β· BSD-3-Clause
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms