Linux Terminals Output Flags (c_oflag) & Control Flags

 

Linux Terminals โ€“ Chapter 62.5
Terminal Flags | Part 2: Output Flags (c_oflag) & Control Flags (c_cflag)
๐Ÿ“š Part 2 of 4
๐Ÿ”ง c_oflag & c_cflag
๐Ÿ–ฅ๏ธ Hardware Settings

Quick Recap

In Part 1 we covered how c_iflag transforms characters coming in from the keyboard before they reach your program. In this part we cover:

  • c_oflag โ€” controls what happens to characters going out to the terminal display
  • c_cflag โ€” controls hardware-level settings like baud rate, character size, parity bits, and flow control

Most c_oflag and c_cflag flags were designed for physical serial terminals. On modern Linux systems (virtual consoles, pseudoterminals), many of them are historical relics with no real effect. This tutorial focuses on the ones that still matter.

Keywords in This Part:

c_oflag OPOST ONLCR OCRNL TABDLY c_cflag CSIZE PARENB CSTOPB HUPCL CRTSCTS baud rate

Output Flags โ€” c_oflag

c_oflag controls how characters are processed after your program calls write() but before they appear on the terminal screen. The master switch for all output processing is the OPOST flag.

Rule: If OPOST is OFF, all other c_oflag flags are ignored. Characters go to the terminal completely raw, with no translation.
Flag Default What it does (plain English)
OPOST ON Master enable for output processing. If OFF, no output flags apply โ€” raw mode.
ONLCR ON Map NL (\n) to CR+NL (\r\n) on output. This is why printf(“\n”) moves to start of next line on screen.
OCRNL OFF Map CR to NL on output. Usually off; conflicts with ONLCR.
ONOCR OFF Don’t output CR if the cursor is already at column 0 (start of line) โ€” saves a redundant move
ONLRET OFF Assume NL already performs CR (cursor return to column 0). Rarely used.
OLCUC OFF Map lowercase output to uppercase. Legacy flag for uppercase-only terminals.
OFILL OFF Use fill characters (padding bytes) for delay instead of timed waits. Historical.
OFDEL OFF Use DEL (0x7F) as fill character; otherwise NUL (0x00). Only relevant if OFILL is set.
TABDLY TAB0 Tab delay mask. Setting TAB3 converts tab characters to spaces (up to 8) on output.
NLDLY NL0 Newline delay mask (NL0, NL1). Historical for slow printers.
CRDLY CR0 CR delay mask (CR0โ€“CR3). Historical for slow carriage-return mechanisms.
BSDLY BS0 Backspace delay mask. Historical.
FFDLY FF0 Form-feed delay mask. Historical.
VTDLY VT0 Vertical-tab delay mask. Historical.

ONLCR โ€” Why printf(“\n”) Moves to Column 0

This is one of the most important output flags to understand. On a physical terminal, a Newline (\n) only moves the cursor down one line โ€” it does NOT move it back to column 0. A Carriage Return (\r) is what moves the cursor to column 0.

So to go to the start of the next line, a terminal needs to receive CR+NL (\r\n). When ONLCR is ON (the default), the terminal driver automatically converts every \n your program writes into \r\n going out to the screen.

Program writes: \n โ†’ ONLCR converts to: \r\n โ†’ Terminal: cursor moves to start of next line

If you turn off ONLCR, your output will look staircase-shaped because each \n only moves down, not back to column 0:

/* What you see with ONLCR OFF: */
Line one
         Line two
                  Line three
/* Each line starts where the previous ended (only moved down, not back to col 0) */
Historical Note: The delay masks (NLDLY, CRDLY, BSDLY, etc.) were used for old mechanical terminals that needed time for the printhead to physically move (e.g., a carriage return could take 0.25 seconds). Modern terminals process these instantly, so the delays are unused on Linux.

TABDLY โ€” Tab Expansion (TAB3)

The TABDLY flag is a mask that can be set to TAB0, TAB1, TAB2, or TAB3. The only practically useful value on Linux is TAB3:

  • TAB0 (default) โ€” no processing, tab characters are sent as-is
  • TAB3 โ€” expand each tab character to the appropriate number of spaces (up to 8) based on the current column position
/* Enable tab expansion in output */
struct termios tp;
tcgetattr(STDOUT_FILENO, &tp);

/* Set TABDLY field to TAB3 (expand tabs to spaces) */
tp.c_oflag &= ~TABDLY;       /* Clear current TABDLY bits */
tp.c_oflag |= TAB3;          /* Set TAB3 */

tcsetattr(STDOUT_FILENO, TCSADRAIN, &tp);
/* Now every \t written will become spaces aligned to 8-column tab stops */

Control Flags โ€” c_cflag

c_cflag controls the low-level serial hardware settings. On modern systems (virtual consoles, network terminals) most of these have no real effect, but they are critical for real serial port (UART) programming โ€” common in embedded systems work.

Flag/Mask Default What it controls
CBAUD B38400 Baud rate mask. Use cfsetispeed() / cfsetospeed() to set baud rate properly.
CBAUDEX OFF Extended baud rate mask for speeds above 38400 (115200, 230400, etc.)
CSIZE CS8 Character size mask: CS5 (5 bits), CS6 (6 bits), CS7 (7 bits), CS8 (8 bits per character)
CSTOPB OFF Use 2 stop bits per character. If OFF (default), uses 1 stop bit.
CREAD ON Enable receiver. If OFF, incoming data is discarded by hardware. Always leave ON.
PARENB OFF Enable parity generation/checking. Works with PARODD to choose odd or even parity.
PARODD OFF If PARENB is ON: OFF = even parity, ON = odd parity
HUPCL ON Hang up (drop modem connection / lower DTR signal) when the last process closes the terminal
CLOCAL OFF Ignore modem status lines. Set this for a direct (non-modem) serial connection so the driver doesn’t wait for carrier signal.
CRTSCTS OFF Enable RTS/CTS hardware flow control. Used in embedded serial comms to prevent buffer overflow.
CMSPAR OFF Use “stick” (mark/space) parity โ€” niche use, rarely seen
CIBAUD (OFF) Input baud rate when different from output. Unused on Linux.

Understanding c_cflag in Embedded Serial Communication

In embedded systems, serial (UART) communication is very common. The c_cflag settings directly map to how the UART hardware frames each byte on the wire:

Start Bit Data Bits (CSIZE) Parity Bit (PARENB) Stop Bit(s) (CSTOPB)
1 bit (always) 5, 6, 7, or 8 bits 0 or 1 bit 1 or 2 bits
Common UART config: 8N1 = 8 data bits (CS8), No parity (PARENB OFF), 1 stop bit (CSTOPB OFF)

Flow Control: Software vs Hardware

Type Flag Mechanism Where configured
Software (XON/XOFF) IXON / IXOFF Special characters (Ctrl+S, Ctrl+Q) in the data stream c_iflag
Hardware (RTS/CTS) CRTSCTS Dedicated hardware pins (RTS, CTS) signal ready/not-ready c_cflag

Hardware flow control (CRTSCTS) is more reliable because it doesn’t occupy any data bandwidth. It is the standard choice in embedded serial communication between microcontrollers and Linux hosts.

Code Example: Configuring a Serial Port (UART) for Embedded Use

This is a complete example of opening a serial port and setting it up for 8N1 at 115200 baud โ€” the most common configuration for embedded Linux serial communication.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>

int open_serial_port(const char *device)
{
    int fd;
    struct termios tty;

    /* Open the serial port */
    fd = open(device, O_RDWR | O_NOCTTY | O_SYNC);
    if (fd < 0) {
        perror("open serial port");
        return -1;
    }

    /* Read current settings */
    if (tcgetattr(fd, &tty) != 0) {
        perror("tcgetattr");
        close(fd);
        return -1;
    }

    /* ---- c_cflag: Hardware settings ---- */

    /* Set baud rate to 115200 (input and output) */
    cfsetispeed(&tty, B115200);
    cfsetospeed(&tty, B115200);

    /* 8 data bits: clear CSIZE field, then set CS8 */
    tty.c_cflag &= ~CSIZE;
    tty.c_cflag |= CS8;

    /* No parity: clear PARENB */
    tty.c_cflag &= ~PARENB;

    /* 1 stop bit: clear CSTOPB */
    tty.c_cflag &= ~CSTOPB;

    /* Enable receiver, ignore modem control lines */
    tty.c_cflag |= (CREAD | CLOCAL);

    /* Disable hardware flow control */
    tty.c_cflag &= ~CRTSCTS;

    /* ---- c_iflag: Disable input processing ---- */
    /* Disable CR/NL mapping, parity check, strip โ€” raw input */
    tty.c_iflag &= ~(ICRNL | INLCR | IGNCR);
    tty.c_iflag &= ~(IXON | IXOFF | IXANY);  /* No software flow control */
    tty.c_iflag &= ~(INPCK | ISTRIP);

    /* ---- c_oflag: Disable output processing ---- */
    tty.c_oflag &= ~OPOST;   /* Raw output โ€” no CR/NL translation */

    /* ---- c_lflag: Disable line discipline processing ---- */
    tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

    /* Non-blocking read: return immediately if no data */
    tty.c_cc[VMIN]  = 0;
    tty.c_cc[VTIME] = 10;   /* 1 second timeout (in 0.1s units) */

    /* Apply settings */
    if (tcsetattr(fd, TCSANOW, &tty) != 0) {
        perror("tcsetattr");
        close(fd);
        return -1;
    }

    return fd;
}

int main(void)
{
    int fd = open_serial_port("/dev/ttyS0");
    if (fd < 0)
        return 1;

    printf("Serial port opened and configured: 115200 8N1\n");

    /* Write a test message */
    const char *msg = "Hello from Linux UART!\r\n";
    write(fd, msg, strlen(msg));

    /* Read response */
    char buf[128];
    int n = read(fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        printf("Received: %s\n", buf);
    }

    close(fd);
    return 0;
}
Why OPOST is cleared for serial ports: When communicating with embedded devices, you don’t want the OS adding extra \r before every \n. You control the exact bytes sent. Clearing OPOST gives you raw output with no automatic translations.

Code Example: Reading Current Baud Rate

The correct way to read or write baud rate is through the helper functions cfgetispeed() and cfgetospeed() โ€” not by reading c_cflag directly.

#include <stdio.h>
#include <termios.h>
#include <unistd.h>

/* Helper: convert speed_t constant to human-readable string */
const char *baud_to_str(speed_t speed)
{
    switch (speed) {
        case B0:      return "0 (Hangup)";
        case B9600:   return "9600";
        case B19200:  return "19200";
        case B38400:  return "38400";
        case B57600:  return "57600";
        case B115200: return "115200";
        case B230400: return "230400";
        default:      return "unknown";
    }
}

int main(void)
{
    struct termios tty;

    if (tcgetattr(STDIN_FILENO, &tty) != 0) {
        perror("tcgetattr");
        return 1;
    }

    speed_t ispeed = cfgetispeed(&tty);
    speed_t ospeed = cfgetospeed(&tty);

    printf("Input baud rate:  %s\n", baud_to_str(ispeed));
    printf("Output baud rate: %s\n", baud_to_str(ospeed));

    return 0;
}

Historical Note: Delay Masks and Old Terminals

The various delay flags in c_oflag (NLDLY, CRDLY, BSDLY, FFDLY, VTDLY) were needed for old electromechanical terminals like teletypes and dot-matrix printers. These devices had printhead mechanisms that physically moved across the paper, and they needed a short pause after a carriage return or newline to allow the mechanism to complete its movement before the next character arrived.

Modern virtual terminals and pseudoterminals process characters at memory speed, so these delays are completely irrelevant. On Linux, most of these flags are recognized by the kernel but have no actual effect. The one exception is TAB3 (part of TABDLY), which still works and converts tab characters to spaces.

Similarly, the IUCLC / OLCUC / XCASE flags were for terminals that could only display uppercase. When a user logged in with an all-uppercase username, the login program would set these flags, causing all output to appear in uppercase and input to be interpreted with a backslash-escape convention for real uppercase characters. These are essentially obsolete today.

Interview Questions โ€” c_oflag & c_cflag

Q1. What is the role of the OPOST flag and what happens if it is disabled?

OPOST is the master enable for output processing. When it is ON (default), the terminal driver applies all c_oflag translations such as NL-to-CR+NL (ONLCR) and tab expansion. When OPOST is OFF, all other c_oflag flags are ignored and data is sent to the terminal exactly as written โ€” raw mode. This is required when writing to serial devices or implementing terminal emulators.

Q2. Why does a printf(“\n”) in C move the cursor to the beginning of the next line, not just down one line?

Because ONLCR is ON by default. The terminal driver converts each outgoing \n into \r\n. The \r moves the cursor to column 0, and the \n moves it down one line. Without this conversion, each \n would only move down and text would appear in a staircase pattern.

Q3. What is 8N1 in serial communication and which termios flags configure it?

8N1 stands for: 8 data bits, No parity, 1 stop bit. It is the most common UART configuration. In termios: CS8 (sets CSIZE to 8 bits), PARENB cleared (no parity), CSTOPB cleared (1 stop bit).

Q4. What is the difference between software flow control (XON/XOFF) and hardware flow control (RTS/CTS)?

Software flow control uses special in-band characters (XOFF=Ctrl+S, XON=Ctrl+Q) in the data stream to pause/resume transmission. It is configured with IXON/IXOFF in c_iflag. Hardware flow control uses dedicated signal lines (RTS โ€” Ready To Send, CTS โ€” Clear To Send) separate from the data lines. It is configured with CRTSCTS in c_cflag. Hardware flow control is more reliable and preferred in embedded systems because it doesn’t consume data bandwidth and works even when the data stream contains 0x11 or 0x13 bytes.

Q5. What does CLOCAL do and when would you set it?

CLOCAL tells the driver to ignore modem status lines (specifically the Carrier Detect / DCD signal). When OFF, the driver waits for a carrier signal before allowing the port to be opened. When ON, it opens immediately regardless. You set CLOCAL for direct (non-modem) serial connections โ€” for example, connecting a microcontroller directly to a Linux host via USB-to-serial or a null-modem cable.

Q6. How do you correctly set baud rate in termios โ€” and why should you avoid manipulating c_cflag directly for baud rate?

The correct way is to use cfsetispeed() and cfsetospeed() with constants like B115200. Directly manipulating the CBAUD bits in c_cflag is non-portable and error-prone because the exact bit layout differs between implementations. The POSIX helper functions abstract this correctly across platforms.

Leave a Reply

Your email address will not be published. Required fields are marked *