Serial / TTY 서브시스템

Serial/TTY 서브시스템을 콘솔, 제어 채널, 산업 통신까지 포함한 입출력 관점에서 심층 분석합니다. UART 하드웨어와 tty core 계층 분리, tty_driver/tty_port 구조, line discipline과 termios 파라미터 적용, RX/TX 버퍼링과 플로우 제어, console/earlycon 부팅 로그 경로, DMA 기반 고속 UART 처리, hangup·재연결·오류 플래그 복구, setserial/stty/debugfs 기반 디버깅까지 현장 장비 운용에 필요한 실전 포인트를 다룹니다.

전제 조건: 디바이스 드라이버Workqueue 문서를 먼저 읽으세요. 입출력 인터페이스 드라이버는 데이터 경로와 제어 경로를 동시에 다루므로 큐/버퍼/비동기 처리 경계를 먼저 구분해야 합니다.
일상 비유: 이 주제는 콜센터 접수와 처리 라인 분리와 비슷합니다. 요청 접수와 실제 처리를 분리해 병목을 줄이듯이, 드라이버도 IRQ·큐·작업 스레드를 역할별로 나눠야 안정적입니다.

핵심 요약

  • 초기화 순서 — 탐색, 바인딩, 자원 등록 순서를 점검합니다.
  • 제어/데이터 분리 — 빠른 경로와 설정 경로를 분리 설계합니다.
  • IRQ/작업 분할 — 즉시 처리와 지연 처리를 구분합니다.
  • 안전 한계 — 전원/열/타이밍 임계값을 함께 관리합니다.
  • 운영 복구 — 오류 시 재초기화와 롤백 경로를 준비합니다.

단계별 이해

  1. 장치 수명주기 확인
    probe부터 remove까지 흐름을 점검합니다.
  2. 비동기 경로 설계
    IRQ, 워크큐, 타이머 역할을 분리합니다.
  3. 자원 정합성 검증
    DMA/클록/전원 참조를 교차 확인합니다.
  4. 현장 조건 테스트
    연결 끊김/복구/부하 상황을 재현합니다.
관련 표준: RS-232C, RS-485, POSIX termios — 시리얼 통신 및 터미널 제어 표준입니다.
관련 페이지: 기본 디바이스 드라이버 모델은 디바이스 드라이버, 버스 서브시스템은 커널 버스 서브시스템 페이지를 참고하세요.

Serial / TTY 서브시스템

TTY(Teletypewriter) 서브시스템은 리눅스 커널에서 가장 오래되고 복잡한 계층 중 하나입니다. 원래 물리적 텔레타이프 단말기를 위해 설계되었지만, 현대 리눅스에서는 시리얼 포트, 가상 콘솔, 의사 터미널(PTY), USB 시리얼 등 다양한 문자 기반 I/O 인터페이스를 통합 관리합니다.

/dev/ttyS0 /dev/ttyUSB0 /dev/pts/N /dev/ttyN /dev/console TTY Core (drivers/tty/tty_io.c) tty_struct · tty_driver · tty_port · tty_operations Line Discipline (N_TTY, SLIP, PPP, ...) tty_ldisc · tty_ldisc_ops · read/write/ioctl serial_core uart_driver / uart_port USB Serial usb_serial_driver PTY master ↔ slave VT Console vt.c / keyboard.c 기타 8250 / PL011 / IMX FTDI / CP210x / CH341 UART 16550A RS-232 / RS-485 ARM PL011 AMBA UART USB-Serial FTDI / CP2102 SoC UART IMX / Samsung Virtual virtio-console

TTY 코어 데이터 구조

TTY 서브시스템의 핵심 구조체들은 include/linux/tty.hinclude/linux/tty_driver.h에 정의되어 있습니다. 각 구조체의 역할과 관계를 이해하는 것이 TTY/Serial 드라이버 개발의 출발점입니다.

#include <linux/tty.h>
#include <linux/tty_driver.h>
#include <linux/tty_flip.h>

/* === tty_struct: 열린 TTY 디바이스 인스턴스 ===
 * 프로세스가 /dev/ttyS0 등을 open()하면 생성됩니다.
 * 하나의 물리 포트에 대해 최대 하나의 tty_struct가 존재합니다.
 */
struct tty_struct {
    int                     magic;       /* TTY_MAGIC — 유효성 검증 */
    struct kref             kref;        /* 참조 카운터 */
    struct device           *dev;        /* sysfs 디바이스 */
    struct tty_driver       *driver;     /* 소속 드라이버 */
    const struct tty_operations *ops;    /* 드라이버 오퍼레이션 */
    int                     index;       /* 드라이버 내 포트 인덱스 */
    struct tty_ldisc        *ldisc;      /* 현재 line discipline */
    struct tty_port         *port;       /* 하드웨어 포트 정보 */
    struct tty_struct       *link;       /* PTY master ↔ slave 연결 */
    struct tty_bufhead      buf;         /* flip buffer 헤드 */
    struct winsize          winsize;     /* 터미널 윈도우 크기 */
    struct ktermios         termios;     /* 현재 termios 설정 */
    unsigned long           flags;       /* TTY_THROTTLED 등 상태 플래그 */
    struct work_struct      hangup_work; /* hangup 지연 처리 */
    struct work_struct      SAK_work;    /* Secure Attention Key */
    /* ... */
};

/* === tty_driver: TTY 드라이버 등록 정보 ===
 * 동일 유형의 여러 포트를 관리하는 드라이버 단위 구조체입니다.
 * 예: 8250 드라이버가 4개 시리얼 포트를 관리할 때 하나의 tty_driver를 등록합니다.
 */
struct tty_driver {
    struct cdev             *cdevs;     /* 문자 디바이스 배열 */
    const char              *driver_name; /* 예: "serial" */
    const char              *name;       /* 디바이스 이름 prefix: "ttyS" */
    int                     name_base;   /* 번호 시작값 (보통 0) */
    int                     major;       /* major 번호 (4=ttyS) */
    int                     minor_start; /* minor 시작 (64=ttyS0) */
    unsigned int            num;         /* 관리 포트 수 */
    short                   type;        /* TTY_DRIVER_TYPE_SERIAL 등 */
    short                   subtype;     /* SERIAL_TYPE_NORMAL 등 */
    struct ktermios         init_termios; /* 초기 termios */
    const struct tty_operations *ops;    /* 드라이버 콜백 */
    struct tty_port         **ports;     /* 포트 배열 */
    /* ... */
};

/* === tty_port: 물리/가상 포트 상태 관리 ===
 * 하드웨어 포트(또는 가상 포트)의 라이프사이클과 상태를 추적합니다.
 * open/close, hangup, carrier detect 등의 동기화를 담당합니다.
 */
struct tty_port {
    struct tty_bufhead     buf;          /* flip buffer */
    struct tty_struct      *tty;         /* 현재 열린 tty */
    const struct tty_port_operations *ops; /* 포트 콜백 */
    struct mutex           mutex;        /* open/close 직렬화 */
    struct mutex           buf_mutex;    /* 버퍼 접근 보호 */
    unsigned long          flags;        /* ASYNC_* 플래그 */
    int                    count;        /* open 참조 카운터 */
    struct wait_queue_head open_wait;    /* carrier detect 대기 */
    struct wait_queue_head close_wait;   /* 닫기 완료 대기 */
    struct wait_queue_head delta_msr_wait; /* 모뎀 상태 변화 대기 */
    unsigned char          console:1;    /* 콘솔 포트 여부 */
    /* ... */
};

TTY 오퍼레이션 — tty_operations

tty_operations는 TTY 드라이버가 TTY 코어에 제공하는 콜백 함수 테이블입니다. VFS의 file_operations와 유사한 패턴으로, user space의 open()/write()/ioctl() 호출이 이 콜백으로 전달됩니다.

struct tty_operations {
    /* 포트 열기/닫기 — 리소스 할당/해제 */
    int  (*open)(struct tty_struct *tty, struct file *filp);
    void (*close)(struct tty_struct *tty, struct file *filp);

    /* 데이터 송신 */
    ssize_t (*write)(struct tty_struct *tty,
                     const u8 *buf, size_t count);
    unsigned int (*write_room)(struct tty_struct *tty); /* 쓰기 가능 바이트 */
    unsigned int (*chars_in_buffer)(struct tty_struct *tty);

    /* termios 설정 변경 (보레이트, 데이터 비트, 패리티 등) */
    void (*set_termios)(struct tty_struct *tty,
                         const struct ktermios *old);

    /* 흐름 제어 */
    void (*throttle)(struct tty_struct *tty);   /* 수신 일시 정지 */
    void (*unthrottle)(struct tty_struct *tty); /* 수신 재개 */
    void (*stop)(struct tty_struct *tty);       /* 송신 정지 (^S) */
    void (*start)(struct tty_struct *tty);      /* 송신 재개 (^Q) */

    /* ioctl 핸들러 */
    int  (*ioctl)(struct tty_struct *tty,
                  unsigned int cmd, unsigned long arg);

    /* 모뎀 제어 신호 (DTR, RTS, CTS, DCD, DSR, RI) */
    int  (*tiocmget)(struct tty_struct *tty);
    int  (*tiocmset)(struct tty_struct *tty,
                     unsigned int set, unsigned int clear);

    /* break 신호 송신 */
    int  (*break_ctl)(struct tty_struct *tty, int state);

    /* 하드웨어 hangup 감지 */
    void (*hangup)(struct tty_struct *tty);

    /* RS485 설정 (half-duplex 산업용 통신) */
    int  (*get_serial)(struct tty_struct *tty,
                       struct serial_struct *p);
    int  (*set_serial)(struct tty_struct *tty,
                       struct serial_struct *p);
    /* ... */
};

Flip Buffer — 수신 데이터 경로

인터럽트 핸들러에서 수신된 데이터를 user space로 전달하는 메커니즘입니다. ISR(Interrupt Service Routine)에서 직접 user space 버퍼에 복사할 수 없으므로, 커널 내부의 flip buffer를 거칩니다. ISR은 flip buffer에 데이터를 채우고, workqueue를 통해 line discipline으로 전달됩니다.

#include <linux/tty_flip.h>

/* 인터럽트 핸들러에서 수신 데이터 처리 */
static irqreturn_t my_uart_irq(int irq, void *dev_id)
{
    struct uart_port *port = dev_id;
    struct tty_port *tport = &port->state->port;
    unsigned int status, ch;

    status = readl(port->membase + REG_STATUS);

    /* 수신 데이터 처리 */
    while (status & RX_DATA_READY) {
        ch = readl(port->membase + REG_DATA);
        unsigned int flag = TTY_NORMAL;

        port->icount.rx++;

        /* 에러 검출 */
        if (status & PARITY_ERR) {
            port->icount.parity++;
            flag = TTY_PARITY;
        } else if (status & FRAME_ERR) {
            port->icount.frame++;
            flag = TTY_FRAME;
        } else if (status & OVERRUN_ERR) {
            port->icount.overrun++;
            flag = TTY_OVERRUN;
        } else if (status & BREAK_DETECT) {
            port->icount.brk++;
            flag = TTY_BREAK;
            if (uart_handle_break(port))
                continue;
        }

        /* sysrq / null char 처리 */
        if (uart_handle_sysrq_char(port, ch))
            continue;

        /* flip buffer에 한 바이트 삽입 */
        tty_insert_flip_char(tport, ch, flag);

        status = readl(port->membase + REG_STATUS);
    }

    /* flip buffer의 데이터를 line discipline으로 push
     * 내부적으로 work를 스케줄하여 ldisc->receive_buf() 호출 */
    tty_flip_buffer_push(tport);

    /* 송신 처리 */
    if (status & TX_EMPTY)
        my_handle_tx(port);

    return IRQ_HANDLED;
}

/* flip buffer API 요약:
 * tty_insert_flip_char(port, ch, flag)   — 1바이트 삽입
 * tty_insert_flip_string(port, str, len) — 문자열 bulk 삽입 (빠름)
 * tty_prepare_flip_string(port, &p, len) — 직접 포인터 획득 (DMA용)
 * tty_flip_buffer_push(port)             — ldisc로 데이터 전달
 */

Line Discipline (회선 규율)

Line discipline은 TTY 코어와 하위 드라이버 사이에서 데이터를 가공하는 중간 계층입니다. 기본 N_TTY는 canonical/non-canonical 모드 처리, 에코, 시그널 문자(^C, ^Z) 등을 담당합니다. 사용자 정의 line discipline으로 교체하면 시리얼 라인 위에 전용 프로토콜을 구현할 수 있습니다.

Line Discipline번호용도커널 소스
N_TTY0기본 터미널 I/O (canonical/raw 모드)drivers/tty/n_tty.c
N_SLIP1Serial Line IP — 시리얼 위 IP 통신drivers/net/slip/
N_PPP3Point-to-Point Protocoldrivers/net/ppp/
N_GSM071021GSM 멀티플렉싱 (모뎀)drivers/tty/n_gsm.c
N_NULL27모든 데이터를 버림 (테스트용)drivers/tty/n_null.c
N_TRACESINK23디버그 트레이스 데이터 싱크drivers/tty/n_tracesink.c
#include <linux/tty_ldisc.h>

/* 사용자 정의 Line Discipline 예제 — 간단한 패킷 프로토콜 */
static struct tty_ldisc_ops my_ldisc_ops = {
    .owner          = THIS_MODULE,
    .num            = N_MY_LDISC,         /* 고유 번호 (29 이상 사용) */
    .name           = "my_proto",

    /* TTY가 이 ldisc로 전환될 때 */
    .open           = my_ldisc_open,
    .close          = my_ldisc_close,

    /* user space → driver 방향: write() 시스템콜에서 호출 */
    .write          = my_ldisc_write,

    /* driver → user space 방향: ISR → flip buffer → 이 콜백 */
    .receive_buf    = my_ldisc_receive,

    /* user space read()에서 호출 — 가공된 데이터 전달 */
    .read           = my_ldisc_read,

    .ioctl          = my_ldisc_ioctl,
};

/* receive_buf 콜백 — 하드웨어에서 수신된 데이터 처리 */
static void my_ldisc_receive(struct tty_struct *tty,
                             const u8 *data, const u8 *flags,
                             size_t count)
{
    struct my_proto *proto = tty->disc_data;
    size_t i;

    for (i = 0; i < count; i++) {
        if (flags && flags[i] != TTY_NORMAL)
            continue;  /* 에러 바이트 건너뛰기 */

        if (data[i] == MY_FRAME_DELIM) {
            my_process_frame(proto);  /* 프레임 완성 → 처리 */
        } else {
            proto->buf[proto->len++] = data[i];
        }
    }
}

/* 모듈 초기화 시 ldisc 등록 */
static int __init my_ldisc_init(void)
{
    return tty_register_ldisc(&my_ldisc_ops);
}

/* user space에서 ldisc 전환:
 *   int ldisc = N_MY_LDISC;
 *   ioctl(fd, TIOCSETD, &ldisc);  // line discipline 변경
 */

serial_core 프레임워크 — UART 드라이버

serial_core(drivers/tty/serial/serial_core.c)는 UART 하드웨어 드라이버를 위한 표준 프레임워크입니다. 드라이버 개발자는 uart_driver를 등록하고, 각 포트에 대해 uart_portuart_ops를 제공하면 됩니다. TTY 코어와의 연동, line discipline 관리, sysfs 노출 등은 serial_core가 자동 처리합니다.

#include <linux/serial_core.h>
#include <linux/platform_device.h>

#define MY_UART_NR       4    /* 지원 포트 수 */
#define MY_UART_FIFO_SZ  64   /* TX/RX FIFO 깊이 */

/* === uart_ops: UART 하드웨어 오퍼레이션 === */
static unsigned int my_tx_empty(struct uart_port *port)
{
    /* TX FIFO가 완전히 비었으면 TIOCSER_TEMT 반환 */
    return (readl(port->membase + REG_STATUS) & TX_FIFO_EMPTY)
           ? TIOCSER_TEMT : 0;
}

static void my_set_mctrl(struct uart_port *port, unsigned int mctrl)
{
    u32 val = readl(port->membase + REG_MCR);

    if (mctrl & TIOCM_RTS) val |= MCR_RTS;  else val &= ~MCR_RTS;
    if (mctrl & TIOCM_DTR) val |= MCR_DTR;  else val &= ~MCR_DTR;

    writel(val, port->membase + REG_MCR);
}

static unsigned int my_get_mctrl(struct uart_port *port)
{
    u32 status = readl(port->membase + REG_MSR);
    unsigned int mctrl = 0;

    if (status & MSR_CTS) mctrl |= TIOCM_CTS;
    if (status & MSR_DCD) mctrl |= TIOCM_CAR;  /* Carrier Detect */
    if (status & MSR_DSR) mctrl |= TIOCM_DSR;
    if (status & MSR_RI)  mctrl |= TIOCM_RNG;  /* Ring Indicator */

    return mctrl;
}

static void my_start_tx(struct uart_port *port)
{
    /* TX 인터럽트 활성화 — ISR에서 실제 전송 수행 */
    u32 ier = readl(port->membase + REG_IER);
    ier |= IER_TX_EMPTY;
    writel(ier, port->membase + REG_IER);
}

static void my_stop_tx(struct uart_port *port)
{
    u32 ier = readl(port->membase + REG_IER);
    ier &= ~IER_TX_EMPTY;
    writel(ier, port->membase + REG_IER);
}

static void my_stop_rx(struct uart_port *port)
{
    u32 ier = readl(port->membase + REG_IER);
    ier &= ~IER_RX_DATA;
    writel(ier, port->membase + REG_IER);
}

static int my_startup(struct uart_port *port)
{
    int ret;

    /* IRQ 등록 */
    ret = request_irq(port->irq, my_uart_irq,
                      IRQF_SHARED, "my-uart", port);
    if (ret)
        return ret;

    /* FIFO 활성화, RX 인터럽트 활성화 */
    writel(FCR_FIFO_EN | FCR_RX_TRIG_HALF,
           port->membase + REG_FCR);
    writel(IER_RX_DATA, port->membase + REG_IER);

    return 0;
}

static void my_shutdown(struct uart_port *port)
{
    /* 모든 인터럽트 비활성화 */
    writel(0, port->membase + REG_IER);
    free_irq(port->irq, port);
}

static void my_set_termios(struct uart_port *port,
                           struct ktermios *termios,
                           const struct ktermios *old)
{
    unsigned int baud, lcr = 0;

    /* 보레이트 계산 — 클램핑 포함 */
    baud = uart_get_baud_rate(port, termios, old,
                              9600, 4000000);
    unsigned int divisor = uart_get_divisor(port, baud);

    /* 데이터 비트 */
    switch (termios->c_cflag & CSIZE) {
    case CS5: lcr |= LCR_WLEN5; break;
    case CS6: lcr |= LCR_WLEN6; break;
    case CS7: lcr |= LCR_WLEN7; break;
    default:  lcr |= LCR_WLEN8; break;
    }

    /* 정지 비트 */
    if (termios->c_cflag & CSTOPB)
        lcr |= LCR_STOP_2;

    /* 패리티 */
    if (termios->c_cflag & PARENB) {
        lcr |= LCR_PARITY;
        if (!(termios->c_cflag & PARODD))
            lcr |= LCR_EVEN_PARITY;
    }

    /* 하드웨어 레지스터 업데이트 */
    spin_lock_irq(&port->lock);
    uart_update_timeout(port, termios->c_cflag, baud);
    writel(divisor, port->membase + REG_BAUD_DIV);
    writel(lcr, port->membase + REG_LCR);

    /* 에러 무시 마스크 설정 */
    port->read_status_mask = OVERRUN_ERR;
    if (termios->c_iflag & INPCK)
        port->read_status_mask |= PARITY_ERR | FRAME_ERR;
    if (termios->c_iflag & (BRKINT | PARMRK))
        port->read_status_mask |= BREAK_DETECT;

    port->ignore_status_mask = 0;
    if (termios->c_iflag & IGNPAR)
        port->ignore_status_mask |= PARITY_ERR | FRAME_ERR;
    if (termios->c_iflag & IGNBRK)
        port->ignore_status_mask |= BREAK_DETECT;

    spin_unlock_irq(&port->lock);
}

static const char *my_type(struct uart_port *port)
{
    return "MY-UART";
}

static void my_config_port(struct uart_port *port, int flags)
{
    if (flags & UART_CONFIG_TYPE)
        port->type = PORT_MY_UART;
}

static const struct uart_ops my_uart_ops = {
    .tx_empty     = my_tx_empty,
    .set_mctrl    = my_set_mctrl,
    .get_mctrl    = my_get_mctrl,
    .start_tx     = my_start_tx,
    .stop_tx      = my_stop_tx,
    .stop_rx      = my_stop_rx,
    .startup      = my_startup,
    .shutdown     = my_shutdown,
    .set_termios  = my_set_termios,
    .type         = my_type,
    .config_port  = my_config_port,
};

/* === uart_driver: 드라이버 등록 구조체 === */
static struct uart_driver my_uart_drv = {
    .owner       = THIS_MODULE,
    .driver_name = "my-uart",      /* /proc/tty/drivers에 표시 */
    .dev_name    = "ttyMY",        /* 디바이스 이름: /dev/ttyMY0, ttyMY1, ... */
    .major       = 0,              /* 0 = 동적 할당 */
    .minor       = 0,
    .nr          = MY_UART_NR,     /* 최대 포트 수 */
    .cons        = &my_console,    /* 콘솔 구조체 (NULL 가능) */
};

/* === Platform Driver 통합 === */
static int my_uart_probe(struct platform_device *pdev)
{
    struct my_uart_priv *priv;
    struct resource *res;
    int ret;

    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    priv->port.membase = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(priv->port.membase))
        return PTR_ERR(priv->port.membase);

    priv->port.irq      = platform_get_irq(pdev, 0);
    priv->port.ops       = &my_uart_ops;
    priv->port.dev       = &pdev->dev;
    priv->port.type      = PORT_MY_UART;
    priv->port.iotype    = UPIO_MEM32;   /* 32-bit MMIO */
    priv->port.fifosize  = MY_UART_FIFO_SZ;
    priv->port.flags     = UPF_BOOT_AUTOCONF;
    priv->port.line      = pdev->id;      /* 포트 번호 (DT: alias) */

    /* 클럭 설정 */
    priv->clk = devm_clk_get_enabled(&pdev->dev, NULL);
    if (IS_ERR(priv->clk))
        return PTR_ERR(priv->clk);
    priv->port.uartclk = clk_get_rate(priv->clk);

    platform_set_drvdata(pdev, priv);

    /* serial_core에 포트 등록 → /dev/ttyMYn 생성 */
    ret = uart_add_one_port(&my_uart_drv, &priv->port);
    if (ret)
        return ret;

    dev_info(&pdev->dev, "MY-UART at 0x%lx, irq %d, %d Hz\\n",
             (unsigned long)res->start, priv->port.irq,
             priv->port.uartclk);
    return 0;
}

static void my_uart_remove(struct platform_device *pdev)
{
    struct my_uart_priv *priv = platform_get_drvdata(pdev);
    uart_remove_one_port(&my_uart_drv, &priv->port);
}

static const struct of_device_id my_uart_of_match[] = {
    { .compatible = "vendor,my-uart" },
    { }
};
MODULE_DEVICE_TABLE(of, my_uart_of_match);

static struct platform_driver my_uart_platform_drv = {
    .probe  = my_uart_probe,
    .remove = my_uart_remove,
    .driver = {
        .name = "my-uart",
        .of_match_table = my_uart_of_match,
    },
};

/* 모듈 초기화: uart_driver 등록 → platform_driver 등록 */
static int __init my_uart_init(void)
{
    int ret = uart_register_driver(&my_uart_drv);
    if (ret)
        return ret;

    ret = platform_driver_register(&my_uart_platform_drv);
    if (ret)
        uart_unregister_driver(&my_uart_drv);
    return ret;
}

static void __exit my_uart_exit(void)
{
    platform_driver_unregister(&my_uart_platform_drv);
    uart_unregister_driver(&my_uart_drv);
}

module_init(my_uart_init);
module_exit(my_uart_exit);

시리얼 콘솔과 earlycon

커널 콘솔은 부팅 메시지(printk)를 출력하는 저수준 인터페이스입니다. TTY 서브시스템이 초기화되기 전에도 동작하므로, earlycon으로 부팅 초기부터 디버그 출력이 가능합니다.

#include <linux/console.h>
#include <linux/serial_core.h>

/* === 일반 시리얼 콘솔 === */
static void my_console_write(struct console *co,
                              const char *s, unsigned int count)
{
    struct uart_port *port = &my_ports[co->index];
    unsigned long flags;
    int locked;

    /* 콘솔은 NMI, panic 등에서도 호출될 수 있음
     * trylock 실패 시에도 출력 시도 (디버깅 목적) */
    locked = spin_trylock_irqsave(&port->lock, flags);

    /* uart_console_write()는 '\n' → '\r\n' 변환 포함 */
    uart_console_write(port, s, count, my_console_putchar);

    if (locked)
        spin_unlock_irqrestore(&port->lock, flags);
}

static void my_console_putchar(struct uart_port *port, unsigned char ch)
{
    /* TX FIFO 비어질 때까지 polling (콘솔은 인터럽트 불가) */
    while (!(readl(port->membase + REG_STATUS) & TX_FIFO_EMPTY))
        cpu_relax();
    writel(ch, port->membase + REG_DATA);
}

static int my_console_setup(struct console *co, char *options)
{
    struct uart_port *port = &my_ports[co->index];
    int baud = 115200, bits = 8, parity = 'n', flow = 'n';

    if (options)
        uart_parse_options(options, &baud, &parity, &bits, &flow);

    return uart_set_options(port, co, baud, parity, bits, flow);
}

static struct console my_console = {
    .name    = "ttyMY",      /* console=ttyMY0,115200 */
    .write   = my_console_write,
    .device  = uart_console_device,  /* serial_core 제공 헬퍼 */
    .setup   = my_console_setup,
    .flags   = CON_PRINTBUFFER,       /* 등록 전 버퍼 출력 */
    .index   = -1,                    /* -1 = 커널 파라미터로 결정 */
    .data    = &my_uart_drv,
};

/* === earlycon: 부팅 초기 콘솔 ===
 * TTY/serial_core 초기화 전에 printk 출력 가능.
 * 커널 파라미터: earlycon=my-uart,0x1c28000,115200
 * Device Tree: chosen { stdout-path = "serial0:115200n8"; };
 */
static void my_early_write(struct console *co,
                           const char *s, unsigned int count)
{
    struct earlycon_device *dev = co->data;
    struct uart_port *port = &dev->port;

    uart_console_write(port, s, count, my_early_putchar);
}

static int __init my_early_console_setup(struct earlycon_device *dev,
                                         const char *options)
{
    if (!dev->port.membase)
        return -ENODEV;
    dev->con->write = my_early_write;
    return 0;
}
OF_EARLYCON_DECLARE(my_uart, "vendor,my-uart", my_early_console_setup);
# 커널 부팅 파라미터 예시
console=ttyS0,115200n8         # 표준 시리얼 콘솔
console=ttyAMA0,115200         # ARM PL011
console=tty0                   # VGA 콘솔
console=ttyS0 console=tty0     # 다중 콘솔 (마지막이 /dev/console)

earlycon=uart8250,mmio32,0xfe215040,115200  # earlycon 직접 지정
earlycon                       # DT stdout-path에서 자동 감지

TTY 디바이스 명명 규칙

디바이스경로용도드라이버/서브시스템
ttySN/dev/ttyS08250/16550 호환 시리얼 포트drivers/tty/serial/8250/
ttyAMAN/dev/ttyAMA0ARM AMBA PL011 UARTdrivers/tty/serial/amba-pl011.c
ttyUSBN/dev/ttyUSB0USB-Serial 변환기 (FTDI, CP210x 등)drivers/usb/serial/
ttyACMN/dev/ttyACM0USB CDC ACM (Abstract Control Model)drivers/usb/class/cdc-acm.c
ttyMFDN/dev/ttyMFD0Intel MID (Medfield) UARTdrivers/tty/serial/mfd.c
ttyON/dev/ttyO0TI OMAP UARTdrivers/tty/serial/omap-serial.c
ttySACN/dev/ttySAC0Samsung S3C/S5P UARTdrivers/tty/serial/samsung_tty.c
ttyN/dev/tty1가상 콘솔 (VT)drivers/tty/vt/
pts/N/dev/pts/0의사 터미널 slave (PTY)drivers/tty/pty.c
ptmx/dev/ptmxPTY master 멀티플렉서drivers/tty/pty.c
console/dev/console시스템 콘솔 (마지막 console= 파라미터)커널 코어
ttyGSN/dev/ttyGS0USB Gadget 시리얼 (디바이스 모드)drivers/usb/gadget/function/u_serial.c
ttyLPN/dev/ttyLP0Intel LPSS UART (Low Power)drivers/tty/serial/8250/8250_lpss.c

PTY (Pseudo-Terminal)

의사 터미널은 물리 하드웨어 없이 TTY 인터페이스를 제공합니다. SSH, 터미널 에뮬레이터(xterm, gnome-terminal), screen/tmux 등이 PTY를 사용합니다. master(제어 프로그램 쪽)와 slave(응용 프로세스 쪽)의 쌍으로 동작하며, master에 쓴 데이터가 slave의 입력으로 나타나고, 그 반대도 마찬가지입니다.

Terminal Emulator (xterm, tmux) Shell/프로그램 (bash, sshd) /dev/ptmx master /dev/pts/N slave (N_TTY 적용) pty_write() pty_read()
/* master에 write → slave에서 read 가능 (키보드 입력 시뮬레이션)
 * slave에 write → master에서 read 가능 (프로그램 출력 캡처)
 * slave 쪽에 N_TTY line discipline 적용 (에코, ^C 등 처리)
 */

/* PTY 생성 과정 (user space, POSIX API) */
#include <stdlib.h>
#include <fcntl.h>

int master_fd = posix_openpt(O_RDWR | O_NOCTTY);
grantpt(master_fd);          /* slave 소유권/퍼미션 설정 */
unlockpt(master_fd);         /* slave 잠금 해제 */
char *slave_name = ptsname(master_fd); /* "/dev/pts/3" 등 */

int slave_fd = open(slave_name, O_RDWR);
/* 이제 master_fd ↔ slave_fd 양방향 통신 가능 */
/* 커널의 PTY 구현 핵심 (drivers/tty/pty.c) */

/* master의 write → slave의 입력 버퍼로 전달 */
static ssize_t pty_write(struct tty_struct *tty,
                         const u8 *buf, size_t c)
{
    struct tty_struct *to = tty->link;  /* master→slave 또는 slave→master */

    if (!to || tty_io_error(tty))
        return -EIO;

    /* 상대편의 flip buffer에 데이터 삽입 */
    c = tty_insert_flip_string(&to->port, buf, c);

    if (c)
        tty_flip_buffer_push(&to->port);

    return c;
}

termios 설정 상세

termios 구조체는 TTY 디바이스의 동작 모드를 제어합니다. 입력/출력 처리, 제어 문자, 로컬 모드 등 네 가지 플래그 그룹으로 구성됩니다.

struct ktermios {
    tcflag_t c_iflag;   /* 입력 모드: IGNBRK, ICRNL, IXON, IXOFF ... */
    tcflag_t c_oflag;   /* 출력 모드: OPOST, ONLCR ... */
    tcflag_t c_cflag;   /* 제어 모드: CSIZE, CSTOPB, PARENB, CRTSCTS ... */
    tcflag_t c_lflag;   /* 로컬 모드: ECHO, ICANON, ISIG, IEXTEN ... */
    cc_t     c_cc[NCCS]; /* 제어 문자: VINTR(^C), VEOF(^D), VMIN, VTIME ... */
    speed_t  c_ispeed;  /* 입력 보레이트 */
    speed_t  c_ospeed;  /* 출력 보레이트 */
};

/* c_cflag 주요 비트:
 * CSIZE   — CS5/CS6/CS7/CS8 (데이터 비트)
 * CSTOPB  — 정지 비트 2개 (미설정 시 1개)
 * PARENB  — 패리티 활성화
 * PARODD  — 홀수 패리티 (미설정 시 짝수)
 * CRTSCTS — 하드웨어 흐름 제어 (RTS/CTS)
 * CLOCAL  — 모뎀 제어 무시 (DCD 불필요)
 * CREAD   — 수신 활성화
 * CBAUD   — 보레이트 마스크 (B9600, B115200 등)
 */

/* c_lflag 주요 비트:
 * ICANON  — Canonical 모드 (줄 단위 입력, ^D로 EOF)
 * ECHO    — 입력 에코
 * ECHOE   — Backspace 에코 (지우기)
 * ISIG    — 시그널 문자 활성화 (^C→SIGINT, ^Z→SIGTSTP, ^\→SIGQUIT)
 * IEXTEN  — 확장 입력 처리 (^V → literal next)
 */
# stty로 termios 설정 확인/변경
stty -a -F /dev/ttyS0
# speed 115200 baud; rows 0; columns 0; line = 0;
# intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; ...
# -parenb -parodd cs8 -cstopb cread clocal -crtscts
# -ignbrk -brkint ignpar -ignpar -parmrk -inpck -istrip ...
# opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel ...
# -isig -icanon -iexten -echo -echoe -echok -echonl ...

# 보레이트 변경
stty -F /dev/ttyS0 115200

# 8N1 설정 (8 데이터 비트, 패리티 없음, 1 정지 비트)
stty -F /dev/ttyS0 cs8 -parenb -cstopb

# Raw 모드 (line discipline 가공 없이 바이트 그대로)
stty -F /dev/ttyS0 raw

# 하드웨어 흐름 제어 활성화
stty -F /dev/ttyS0 crtscts

# 소프트웨어 흐름 제어 (XON/XOFF)
stty -F /dev/ttyS0 ixon ixoff

RS-485 모드

RS-485는 산업용 half-duplex 직렬 통신 표준으로, 하나의 버스에 여러 디바이스를 연결합니다. 리눅스 커널은 serial_rs485 구조체와 TIOCSRS485 ioctl을 통해 RS-485 모드를 지원합니다.

#include <linux/serial.h>

/* user space에서 RS-485 모드 활성화 */
struct serial_rs485 rs485conf = {
    .flags = SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND,
    .delay_rts_before_send = 0,  /* TX 시작 전 RTS 지연 (ms) */
    .delay_rts_after_send  = 0,  /* TX 완료 후 RTS 지연 (ms) */
};
ioctl(fd, TIOCSRS485, &rs485conf);

/* 커널 UART 드라이버에서 RS-485 지원:
 * uart_port.rs485_config() 콜백 구현 필요.
 * RTS 핀을 TX enable로 사용하여 송신 시 RTS 활성화,
 * 수신 시 RTS 비활성화하여 트랜시버 방향 제어.
 *
 * Device Tree 설정 예:
 *   &uart1 {
 *       linux,rs485-enabled-at-boot-time;
 *       rs485-rts-delay = <0 0>;
 *       rs485-rts-active-low;          // RTS 극성 반전
 *   };
 */

TTY/Serial 디버깅

# ─── 시스템 정보 확인 ───

# 등록된 TTY 드라이버 목록
cat /proc/tty/drivers
# /dev/tty             /dev/tty        5       0 system:/dev/tty
# /dev/console         /dev/console    5       1 system:console
# /dev/ptmx            /dev/ptmx       5       2 system
# serial               /dev/ttyS       4  64-67 serial
# pty_slave            /dev/pts      136   0-... pty:slave
# pty_master           /dev/ptm      128   0-... pty:master

# 활성 TTY 라인 정보
cat /proc/tty/line_disc
# n_tty       0

# 시리얼 포트 하드웨어 정보
cat /proc/tty/driver/serial
# serinfo:1.0 driver revision:
# 0: uart:16550A port:000003F8 irq:4 tx:0 rx:0
# 1: uart:16550A port:000002F8 irq:3 tx:0 rx:0

# setserial로 시리얼 포트 상세 정보
setserial -g /dev/ttyS0
# /dev/ttyS0, UART: 16550A, Port: 0x03f8, IRQ: 4

# ─── 디바이스 테스트 ───

# minicom 또는 picocom으로 시리얼 통신
minicom -D /dev/ttyS0 -b 115200
picocom --baud 115200 /dev/ttyS0

# 간단한 시리얼 송수신 테스트
echo "hello" > /dev/ttyS0             # 데이터 송신
cat /dev/ttyS0                          # 데이터 수신 (blocking)
dd if=/dev/ttyS0 bs=1 count=10          # 10바이트만 수신

# ─── 커널 디버깅 ───

# TTY 관련 커널 로그
dmesg | grep -i -E 'tty|serial|uart'
# [    0.000000] printk: console [tty0] enabled
# [    0.524130] serial8250: ttyS0 at I/O 0x3f8 (irq = 4, base_baud = 115200)
# [    1.234567] usb 1-1: FTDI USB Serial Device converter now attached to ttyUSB0

# dynamic debug로 serial_core 트레이싱
echo 'module serial_core +p' > /sys/kernel/debug/dynamic_debug/control
echo 'module 8250_core +p' > /sys/kernel/debug/dynamic_debug/control

# UART 포트 통계 (인터럽트 카운터)
cat /proc/interrupts | grep -i serial
#  4:       128   IO-APIC   4-edge      serial

# sysfs를 통한 UART 정보
ls /sys/class/tty/ttyS0/
# close_delay  closing_wait  custom_divisor  io_type  iomem_base
# iomem_reg_shift  irq  line  port  type  uartclk  xmit_fifo_size

cat /sys/class/tty/ttyS0/uartclk   # UART 기본 클럭
cat /sys/class/tty/ttyS0/type      # UART 타입 (16550A=4)

# ─── PTY 정보 ───

# 현재 열린 PTY 확인
ls /dev/pts/
# 0  1  2  ptmx

# 자신의 터미널 확인
tty
# /dev/pts/0

# 프로세스별 controlling terminal
ps -eo pid,tty,comm | head -20
TX 인터럽트 핸들러 패턴: UART 송신은 circular buffer(uart_state->xmit)를 통해 이루어집니다. start_tx()가 TX empty 인터럽트를 활성화하면, ISR에서 uart_circ_chars_pending()으로 남은 데이터를 확인하고 FIFO에 채워넣습니다. 버퍼가 비면 uart_write_wakeup()을 호출하여 대기 중인 write()를 깨우고, 전송 완료 시 stop_tx()로 인터럽트를 끕니다.
/* TX 인터럽트 핸들러 패턴 */
static void my_handle_tx(struct uart_port *port)
{
    struct tty_port *tport = &port->state->port;
    unsigned int pending;
    u8 ch;

    /* x_char (XON/XOFF) 우선 송신 */
    if (port->x_char) {
        writel(port->x_char, port->membase + REG_DATA);
        port->icount.tx++;
        port->x_char = 0;
        return;
    }

    /* pending 데이터를 FIFO에 채워넣기 */
    pending = kfifo_len(&tport->xmit_fifo);
    if (pending == 0 || uart_tx_stopped(port)) {
        my_stop_tx(port);
        return;
    }

    while (readl(port->membase + REG_STATUS) & TX_FIFO_NOT_FULL) {
        if (!kfifo_get(&tport->xmit_fifo, &ch))
            break;
        writel(ch, port->membase + REG_DATA);
        port->icount.tx++;
    }

    /* 버퍼 여유 생기면 write() 대기 프로세스 깨우기 */
    if (kfifo_len(&tport->xmit_fifo) < WAKEUP_CHARS)
        uart_write_wakeup(port);

    /* 모든 데이터 전송 완료 시 TX 인터럽트 끄기 */
    if (kfifo_is_empty(&tport->xmit_fifo))
        my_stop_tx(port);
}
TTY/Serial 드라이버 개발 주의사항:
  • ISR(Interrupt Service Routine) 컨텍스트 — UART 인터럽트 핸들러는 hard IRQ 컨텍스트에서 실행됩니다. sleep, mutex, GFP_KERNEL 할당 불가. spin_lock(&port->lock)으로 start_tx/stop_tx와의 경쟁 보호
  • flip buffer 크기TTY_BUFFER_PAGE(4KB) 단위로 할당됩니다. 고속 통신에서 ISR이 지연되면 데이터 손실 발생 가능. DMA 전송 사용 권장
  • hangup 경쟁 — USB-Serial 언플러그 시 hangup()write()가 동시에 호출될 수 있음. tty_port_hangup() 사용으로 안전한 처리 보장
  • DMA 전송 — 고속 UART에서는 PIO(Programmed I/O) 대신 DMA 사용 권장. tty_prepare_flip_string()으로 직접 DMA 타겟 버퍼 획득 가능
  • 콘솔 write 경로console_write()는 printk에서 호출되므로 NMI, panic 등 어떤 컨텍스트에서든 안전해야 합니다. spin_trylock() 사용 필수
  • suspend/resumeuart_suspend_port()/uart_resume_port() 사용. 진행 중인 DMA 전송 중지, TX FIFO drain 대기, 클럭 재설정 등 순서 준수 필수

8250/16550 드라이버 — 가장 보편적인 UART

8250/16550 호환 UART는 PC 시리얼 포트의 사실상 표준입니다. 리눅스 커널의 drivers/tty/serial/8250/ 디렉터리에 구현되어 있으며, PCI, ACPI, Device Tree, ISA 등 다양한 열거 방식을 지원합니다.

레지스터오프셋읽기 용도쓰기 용도
RBR/THR0x00수신 데이터 (RBR)송신 데이터 (THR)
IER0x01인터럽트 활성화 (RX, TX, Line Status, Modem Status)
IIR/FCR0x02인터럽트 식별 (IIR)FIFO 제어 (FCR)
LCR0x03Line Control (데이터 비트, 정지 비트, 패리티, DLAB)
MCR0x04Modem Control (DTR, RTS, loopback)
LSR0x05Line Status (Data Ready, Overrun, Parity Err, TX Empty)
MSR0x06Modem Status (CTS, DSR, RI, DCD 변화 감지)
SCR0x07Scratch Register (UART 존재 감지용)
DLL/DLM0x00/0x01Divisor Latch (DLAB=1일 때, 보레이트 설정)
/* 8250 포트를 수동으로 등록하는 예 (레거시/커스텀 보드) */
#include <linux/serial_8250.h>

static struct plat_serial8250_port my_8250_data[] = {
    {
        .mapbase  = 0x3F8,           /* COM1 물리 주소 */
        .irq      = 4,
        .uartclk  = 1843200,         /* 1.8432 MHz 기본 클럭 */
        .iotype   = UPIO_PORT,        /* x86 I/O 포트 접근 */
        .flags    = UPF_SKIP_TEST | UPF_BOOT_AUTOCONF,
        .regshift = 0,               /* 레지스터 간격: 1바이트 */
    },
    { }, /* 터미네이터 */
};

/* 보레이트 계산:
 * divisor = uartclk / (16 × baud_rate)
 * 115200 baud: 1843200 / (16 × 115200) = 1
 * 9600 baud:   1843200 / (16 × 9600) = 12
 */
서브시스템주요 드라이버디버깅 도구
Inputgpio-keys, atkbd, hid-*evtest, libinput debug-events
USBxhci-hcd, ehci-hcd, usb-storagelsusb -v, usbmon
V4L2uvcvideo, vividv4l2-ctl, media-ctl
DRMi915, amdgpu, nouveaumodetest, drm_info
ALSAsnd-hda-intel, snd-usb-audioaplay -l, alsamixer
Serial8250, pl011, imx-uartminicom, stty

이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.