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 — 시리얼 통신 및 터미널 제어 표준입니다.
관련 페이지: 기본 디바이스 드라이버 모델은 디바이스 드라이버, 버스 프레임워크는 I2C/SPI/GPIO 페이지를 참고하세요.

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

USB Serial 서브시스템

USB-Serial 변환기(FTDI, CP210x, CH341, PL2303 등)는 drivers/usb/serial/에 구현됩니다. USB Serial 서브시스템은 USB 버스 위에 TTY 인터페이스를 제공하는 중간 계층으로, usb_serial_driver 구조체를 통해 각 변환 칩에 맞는 콜백을 등록합니다.

User Space Application → open(/dev/ttyUSB0) → read/write Kernel Space TTY Core + Line Discipline (N_TTY) USB Serial Core (usb-serial.c) ftdi_sio FT232R/FT2232 cp210x CP2102/CP2104 ch341 CH340/CH341 pl2303 PL2303TA generic bulk 전용 USB Core (HCD → xHCI/EHCI/OHCI) Hardware USB-Serial Converter IC RS-232/RS-485 트랜시버
#include <linux/usb.h>
#include <linux/usb/serial.h>

/* USB Serial 드라이버 구조체 — 각 변환 칩 드라이버가 등록 */
static struct usb_serial_driver my_usb_serial_device = {
    .driver = {
        .owner = THIS_MODULE,
        .name  = "my-usb-serial",
    },
    .description      = "My USB Serial Adapter",
    .id_table         = my_id_table,      /* USB VID/PID 매칭 테이블 */
    .num_ports        = 1,                /* 포트 수 */

    /* 포트 초기화/해제 */
    .port_probe       = my_port_probe,
    .port_remove      = my_port_remove,

    /* TTY operations 매핑 */
    .open             = my_open,           /* DTR/RTS 활성화, bulk URB 제출 */
    .close            = my_close,          /* URB 취소, DTR/RTS 비활성화 */
    .write            = my_write,          /* 데이터 → bulk OUT URB */
    .write_room       = my_write_room,

    /* termios → 칩 레지스터 변환 */
    .set_termios      = my_set_termios,    /* 보레이트, 데이터비트 등 */

    /* 모뎀 제어 */
    .tiocmget         = my_tiocmget,
    .tiocmset         = my_tiocmset,
    .dtr_rts          = my_dtr_rts,

    /* break 신호 */
    .break_ctl        = my_break_ctl,

    /* 수신 데이터 처리 (bulk IN 완료 콜백에서 호출) */
    .process_read_urb = my_process_read_urb,

    /* USB suspend/resume */
    .suspend          = my_suspend,
    .resume           = my_resume,
};

static const struct usb_device_id my_id_table[] = {
    { USB_DEVICE(0x1234, 0x5678) },  /* VID, PID */
    { }
};
MODULE_DEVICE_TABLE(usb, my_id_table);

static struct usb_serial_driver * const serial_drivers[] = {
    &my_usb_serial_device, NULL
};
module_usb_serial_driver(serial_drivers, my_id_table);

/* === 수신 콜백: bulk IN URB 완료 → flip buffer → ldisc === */
static void my_process_read_urb(struct urb *urb)
{
    struct usb_serial_port *port = urb->context;
    unsigned char *data = urb->transfer_buffer;
    int len = urb->actual_length;

    if (len == 0)
        return;

    /* 칩별 상태 바이트 처리 (FTDI: 2바이트 헤더 스킵) */
    /* data += header_size; len -= header_size; */

    tty_insert_flip_string(&port->port, data, len);
    tty_flip_buffer_push(&port->port);
}

/* === 송신: write() → bulk OUT URB 제출 === */
static int my_write(struct tty_struct *tty,
                    struct usb_serial_port *port,
                    const unsigned char *buf, int count)
{
    struct urb *urb;
    int result;

    urb = usb_alloc_urb(0, GFP_ATOMIC);
    if (!urb)
        return -ENOMEM;

    memcpy(port->write_urbs[0]->transfer_buffer, buf, count);
    usb_fill_bulk_urb(urb, port->serial->dev,
                      usb_sndbulkpipe(port->serial->dev,
                                       port->bulk_out_endpointAddress),
                      port->write_urbs[0]->transfer_buffer, count,
                      my_write_callback, port);

    result = usb_submit_urb(urb, GFP_ATOMIC);
    if (result)
        dev_err(&port->dev, "URB submit failed: %d\n", result);

    return count;
}
변환 칩드라이버보레이트 범위GPIO특이사항
FTDI FT232Rftdi_sio300 ~ 3MCBUS GPIO가장 호환성 좋음, bitbang 모드 지원
FTDI FT2232Hftdi_sio300 ~ 12MCBUS듀얼 포트, MPSSE(SPI/I2C/JTAG)
CP2102/CP2104cp210x300 ~ 2MGPIOSilicon Labs, 소형 패키지
CH340/CH341ch34150 ~ 2M-가장 저렴, 중국산 보드에 다수
PL2303TApl230375 ~ 6M-Prolific, 구형 위조칩 이슈
CDC ACMcdc-acm장치 의존-표준 USB 클래스, Arduino/STM32 사용
MCP2221Ahid300 ~ 460KGPIO/ADC/DACHID 기반 (비표준 경로)
USB-Serial Hotplug 주의: USB 케이블 분리 시 tty_hangup()이 비동기로 호출됩니다. 진행 중인 URB는 usb_kill_urb()로 취소해야 하며, disconnect() 콜백에서 모든 자원을 안전하게 해제해야 합니다. port->port.count가 0이 아닌 상태에서 분리되면 -EIO가 반환되며, 사용자 프로세스는 SIGHUP을 수신합니다.
# USB Serial 디버깅 명령
lsusb -v -d 0403:6001          # FTDI 디바이스 상세 정보
dmesg | grep -i 'ttyUSB\|usb.*serial\|ftdi\|cp210x\|ch341'

# udev 규칙으로 고정 디바이스 이름 부여
# /etc/udev/rules.d/99-serial.rules
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", \
  ATTRS{idProduct}=="6001", ATTRS{serial}=="A12345", \
  SYMLINK+="ttyMYDEVICE"

# USB Serial 트레이싱
echo 1 > /sys/kernel/debug/usb/usbmon/1u  # bus 1 모니터링
cat /sys/kernel/debug/usb/usbmon/1u       # URB 트래픽 확인

serdev — Serial Device Bus

serdev(Serial Device Bus)는 커널 5.x에서 도입된 새로운 시리얼 디바이스 프레임워크입니다. 기존에는 시리얼 장치(Bluetooth HCI, GPS, NFC 등)를 사용하려면 user space에서 TTY를 열고 line discipline을 설정해야 했지만, serdev를 사용하면 커널 내에서 직접 시리얼 장치를 드라이버로 바인딩할 수 있습니다.

전통적 방식 (ldisc) User Space Daemon open(/dev/ttyS1) ioctl(TIOCSETD, N_HCI) Line Discipline (N_HCI) UART Driver serdev 방식 (커널 직접) serdev Client Driver serdev_device_ops serdev Controller UART Driver user space 불필요!
#include <linux/serdev.h>

/* serdev 클라이언트 드라이버 예제 — Bluetooth HCI over UART */

/* 수신 콜백: UART에서 데이터가 도착하면 호출 */
static size_t my_serdev_receive(struct serdev_device *serdev,
                                const u8 *data, size_t count)
{
    struct my_device *dev = serdev_device_get_drvdata(serdev);

    /* 프로토콜 파싱 — 수신된 바이트 처리 */
    return my_protocol_parse(dev, data, count);
}

/* 송신 완료 콜백 */
static void my_serdev_write_wakeup(struct serdev_device *serdev)
{
    struct my_device *dev = serdev_device_get_drvdata(serdev);
    schedule_work(&dev->tx_work);
}

static const struct serdev_device_ops my_serdev_ops = {
    .receive_buf  = my_serdev_receive,
    .write_wakeup = my_serdev_write_wakeup,
};

static int my_serdev_probe(struct serdev_device *serdev)
{
    struct my_device *dev;
    int ret;

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

    dev->serdev = serdev;
    serdev_device_set_drvdata(serdev, dev);
    serdev_device_set_client_ops(serdev, &my_serdev_ops);

    /* 보레이트, 흐름제어 설정 */
    ret = serdev_device_open(serdev);
    if (ret)
        return ret;

    serdev_device_set_baudrate(serdev, 3000000);  /* 3Mbps */
    serdev_device_set_flow_control(serdev, true); /* RTS/CTS */
    serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);

    /* 동기식 데이터 송신 */
    ret = serdev_device_write_buf(serdev, init_cmd, sizeof(init_cmd));

    return 0;
}

static void my_serdev_remove(struct serdev_device *serdev)
{
    serdev_device_close(serdev);
}

/* Device Tree 바인딩:
 *   &uart2 {
 *       bluetooth {
 *           compatible = "vendor,my-bt";
 *           max-speed = <3000000>;
 *       };
 *   };
 */
static const struct of_device_id my_serdev_of_match[] = {
    { .compatible = "vendor,my-bt" },
    { }
};

static struct serdev_device_driver my_serdev_driver = {
    .probe  = my_serdev_probe,
    .remove = my_serdev_remove,
    .driver = {
        .name           = "my-serdev",
        .of_match_table = my_serdev_of_match,
    },
};
module_serdev_device_driver(my_serdev_driver);
serdev 사용 사례: hci_serdev.c (Bluetooth HCI UART), gnss-serial.c (GNSS/GPS), nxp-nci-uart.c (NFC), qca_uart.c (Qualcomm BT) 등이 serdev를 사용합니다. 새로운 시리얼 연결 장치는 line discipline 대신 serdev로 구현하는 것이 권장됩니다.

TTY의 역사와 진화

TTY 서브시스템은 리눅스 커널에서 가장 긴 역사를 가진 계층입니다. 1960년대 물리적 텔레타이프 기계에서 시작하여, 현대의 컨테이너 터미널과 SSH까지 이어지는 진화 과정을 이해하면 현재 코드의 복잡성이 왜 존재하는지 납득할 수 있습니다.

1960s Teletype ASR-33 1970s UNIX TTY VT100 단말기 1984 POSIX termios 표준화 1991 Linux TTY serial_core 2004+ devpts 마운트 PTY 네임스페이스 현재 serdev 컨테이너 PTY 물리 단말기 → 가상 터미널 → 의사 터미널 → 컨테이너/원격 → 커널 직접 바인딩(serdev) 하드웨어 의존 → 추상화 계층 → user space 분리 → 커널 내 통합
시대기술핵심 변화커널 영향
1960sTeletype ASR-33기계식 타자기 ↔ 컴퓨터 직렬 연결"TTY" 용어의 기원, 20mA 전류 루프
1970sVT100 / UNIX V7CRT 단말기, 커서 제어(ANSI escape), stty 도입line discipline 개념 확립, struct termio
1984POSIX.1 termios터미널 I/O 표준화, tcgetattr/tcsetattrstruct termios → 커널 ktermios
1991Linux 0.01UNIX TTY 계층을 PC용으로 재구현8250 드라이버, /dev/ttyS0, VT 콘솔
1998UNIX98 PTYptmx/pts 동적 할당 → 고정 pty 디바이스 제거/dev/ptmx + /dev/pts/N
2004devpts filesystemPTY 네임스페이스 분리 (컨테이너 기반)newinstance 마운트 옵션
2017serdev커널 내 시리얼 디바이스 직접 바인딩Bluetooth HCI, GNSS 등 ldisc 불필요
현재TTY 리팩토링kfifo TX 버퍼, tty_port 통합, locking 정리uart_circ_bufkfifo 전환 진행 중

TTY open/close 수명주기

TTY 디바이스를 open()하면 커널 내부에서 복잡한 초기화 체인이 실행됩니다. tty_open()tty_init_dev()driver->ops->open()tty_port_open() 순서로 진행되며, 첫 open 시에만 하드웨어를 활성화하고, 이후 open은 참조 카운터만 증가시킵니다.

open() 경로 sys_open("/dev/ttyS0") tty_open() tty_lookup_driver() major/minor → tty_driver tty_init_dev() alloc_tty_struct 첫 open만 tty_ldisc_init N_TTY 설정 driver->ops->open() → uart_open() tty_port_open() port->ops->activate() startup() — IRQ 등록 FIFO 활성화, 인터럽트 on close() 경로 tty_release() tty_ldisc_release() ldisc 해제 driver->ops->close() → uart_close() tty_port_close() count-- → 마지막 close? shutdown() — IRQ 해제 인터럽트 off, FIFO drain tty_ldisc_kill() ldisc 완전 해제 free_tty_struct() kref → 0 시 해제 hangup: tty_vhangup() → __tty_hangup() DCD 손실, USB 분리 → ldisc flush + SIGHUP 전송 port->ops->shutdown() 호출 후 tty_port_hangup()으로 정리
/* tty_port_operations — 포트 수명주기 콜백 */
static const struct tty_port_operations my_port_ops = {
    /* 첫 open 시 호출 — 하드웨어 활성화 */
    .activate   = my_port_activate,

    /* 마지막 close 시 호출 — 하드웨어 비활성화 */
    .shutdown   = my_port_shutdown,

    /* carrier detect 상태 반환 (DCD) */
    .carrier_raised = my_carrier_raised,

    /* DTR/RTS 제어 */
    .dtr_rts    = my_dtr_rts,

    /* 포트 해제 (kref → 0) */
    .destruct   = my_port_destruct,
};

/* activate: 첫 번째 open()에서만 호출 */
static int my_port_activate(struct tty_port *port,
                            struct tty_struct *tty)
{
    struct uart_port *uport = container_of(...);

    /* 클럭 활성화 */
    clk_prepare_enable(priv->clk);

    /* FIFO 리셋 */
    writel(FCR_FIFO_EN | FCR_RX_RESET | FCR_TX_RESET,
           uport->membase + REG_FCR);

    /* 인터럽트 등록 및 활성화 */
    request_irq(uport->irq, my_uart_irq, IRQF_SHARED,
                "my-uart", uport);
    writel(IER_RX_DATA, uport->membase + REG_IER);

    return 0;
}

/* shutdown: 마지막 close()에서만 호출 */
static void my_port_shutdown(struct tty_port *port)
{
    struct uart_port *uport = container_of(...);

    /* TX FIFO drain 대기 */
    while (!(readl(uport->membase + REG_STATUS) & TX_FIFO_EMPTY))
        cpu_relax();

    /* 인터럽트 비활성화 및 해제 */
    writel(0, uport->membase + REG_IER);
    free_irq(uport->irq, uport);

    /* 클럭 비활성화 */
    clk_disable_unprepare(priv->clk);
}

TTY 잠금과 동기화 모델

TTY 서브시스템은 여러 컨텍스트(user process, IRQ, workqueue, hangup work)에서 동시에 접근되므로 정교한 잠금 계층이 필요합니다. 잠금 순서를 위반하면 데드락이 발생하므로, 커널 문서(Documentation/driver-api/serial/driver.rst)에 명시된 순서를 반드시 준수해야 합니다.

잠금 획득 순서 (위 → 아래, 위반 시 데드락) tty_mutex (전역) tty_open / tty_release 직렬화 tty->legacy_mutex (per-tty) read/write/ioctl/ldisc 변경 보호 port->mutex (per-port) open/close count, activate/shutdown tty->ldisc_sem (rw_semaphore) ldisc 교체 vs 사용 보호 port->lock (spinlock) IRQ 컨텍스트에서 사용 가능 port->buf_mutex flip buffer 할당/해제 xmit_fifo (kfifo) TX circular buffer (lock-free) port->lock은 ISR에서 유일하게 사용 가능한 잠금 — spin_lock_irqsave() 필수
잠금종류보호 대상사용 컨텍스트
tty_mutex전역 mutextty_driver 목록, tty 생성/삭제프로세스
tty->legacy_mutexper-tty mutexread/write/ioctl 직렬화프로세스
port->mutexper-port mutexopen/close count, 상태 전환프로세스
tty->ldisc_semrw_semaphoreline discipline 교체 vs 사용프로세스
port->lockspinlock하드웨어 레지스터, icount, 상태 플래그프로세스 + IRQ
port->buf_mutexmutexflip buffer 할당/해제프로세스
xmit_fifokfifoTX circular buffer (단일 생산자/소비자)lock-free
/* 잠금 사용 패턴 예시 */

/* 1. ISR에서: spin_lock만 사용 (sleep 불가) */
static irqreturn_t uart_irq(int irq, void *dev_id)
{
    struct uart_port *port = dev_id;
    unsigned long flags;

    spin_lock_irqsave(&port->lock, flags);
    /* icount 업데이트, 레지스터 접근 */
    port->icount.rx++;
    spin_unlock_irqrestore(&port->lock, flags);

    return IRQ_HANDLED;
}

/* 2. set_termios에서: port->lock으로 레지스터 보호 */
static void my_set_termios(...)
{
    spin_lock_irq(&port->lock);
    /* 보레이트 divisor, LCR 레지스터 업데이트 */
    writel(divisor, port->membase + REG_BAUD_DIV);
    spin_unlock_irq(&port->lock);
}

/* 3. hangup에서: 잠금 순서 준수
 *    tty->legacy_mutex → port->mutex → port->lock */
static void my_hangup(struct tty_struct *tty)
{
    struct uart_port *uport = ...;

    mutex_lock(&uport->state->port.mutex);
    /* shutdown, 상태 초기화 */
    tty_port_hangup(&uport->state->port);
    mutex_unlock(&uport->state->port.mutex);
}

데이터 흐름 상세 — Write/Read 경로

TTY 서브시스템의 데이터 흐름은 Write(송신)과 Read(수신) 두 방향으로 나뉘며, 각각 다른 버퍼링과 동기화 메커니즘을 사용합니다. 전체 경로를 이해하면 지연(latency), 처리량(throughput), 데이터 손실의 원인을 정확히 진단할 수 있습니다.

Write Path (User → Hardware) write(fd,buf,n) sys_write tty_write() VFS → TTY ldisc->write() N_TTY: OPOST 처리 driver->write() uart_write() xmit_fifo (kfifo) TX circular buffer start_tx() TX IRQ 활성화 TX ISR kfifo → HW FIFO HW TX FIFO 16~128 바이트 Shift Register → TX 핀 출력 Read Path (Hardware → User) RX 핀 입력 → Shift Register HW RX FIFO 트리거 레벨 도달 RX ISR flip_char() 호출 Flip Buffer tty_buffer 체인 ldisc recv N_TTY 처리 read() user
버퍼 단계크기위치오버플로 시
HW RX FIFO1~128 바이트 (UART 모델 의존)UART 하드웨어Overrun Error (LSR bit 1)
Flip Buffer4KB 페이지 단위, 동적 체인커널 메모리 (tty_buffer)메모리 부족 시 데이터 손실
N_TTY read_buf4096 바이트 (N_TTY_BUF_SIZE)ldisc 내부throttle() → RTS 비활성화
xmit_fifo (TX)PAGE_SIZE (4096)커널 kfifowrite() 블로킹 또는 부분 쓰기
HW TX FIFO1~128 바이트UART 하드웨어TX ISR에서 채움

N_TTY 내부 동작 — Canonical/Raw/CBREAK

N_TTY는 기본 line discipline으로, 거의 모든 터미널 세션에서 사용됩니다. 세 가지 주요 모드를 제공하며, 각 모드는 입력 처리 방식을 근본적으로 바꿉니다.

Canonical (ICANON) 입력: 줄 단위 버퍼링 Enter/^D → read() 반환 줄 편집: Backspace, ^U, ^W ECHO: 입력 문자 화면 출력 ISIG: ^C→SIGINT, ^Z→SIGTSTP 사용: 셸, 대화형 프로그램 설정: stty icanon echo isig c_lflag |= ICANON | ECHO | ISIG VEOF=^D, VERASE=^?, VKILL=^U VMIN/VTIME 무시됨 Raw (!ICANON !ISIG !ECHO) 입력: 바이트 단위 즉시 전달 가공 없음 (에코/시그널 X) VMIN: 최소 바이트 수 VTIME: 타임아웃 (1/10초) OPOST도 해제 → 완전 투명 사용: minicom, 시리얼 통신 설정: stty raw 또는 cfmakeraw() c_lflag &= ~(ICANON|ECHO|ISIG) c_iflag &= ~(IXON|ICRNL|...) c_oflag &= ~OPOST CBREAK (!ICANON +ISIG) 입력: 바이트 단위 전달 시그널 문자(^C)는 동작함 줄 편집 비활성화 VMIN/VTIME 유효 ECHO 선택적 활성화 가능 사용: vi, ncurses, less 설정: stty -icanon min 1 time 0 c_lflag &= ~ICANON c_lflag |= ISIG c_cc[VMIN]=1, c_cc[VTIME]=0
/* VMIN/VTIME 조합 — Non-canonical 모드 read() 동작
 *
 * VMIN=0, VTIME=0 : 폴링 — 즉시 반환 (데이터 없으면 0 반환)
 * VMIN>0, VTIME=0 : 블로킹 — VMIN 바이트 수신될 때까지 대기
 * VMIN=0, VTIME>0 : 타이머 — VTIME*0.1초 내 데이터 또는 타임아웃
 * VMIN>0, VTIME>0 : 혼합 — 첫 바이트 후 VTIME 내 VMIN 바이트 대기
 */

/* cfmakeraw() 내부 구현 (glibc) */
void cfmakeraw(struct termios *t)
{
    t->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP |
                    INLCR | IGNCR | ICRNL | IXON);
    t->c_oflag &= ~OPOST;
    t->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    t->c_cflag &= ~(CSIZE | PARENB);
    t->c_cflag |= CS8;
    t->c_cc[VMIN]  = 1;
    t->c_cc[VTIME] = 0;
}

/* N_TTY 핵심 경로: receive_buf → canonical 처리 */
/* drivers/tty/n_tty.c — n_tty_receive_buf_common() */
/*
 * 1. 각 바이트에 대해:
 *    - ISTRIP: 8번째 비트 제거
 *    - IGNCR: CR 무시 / ICRNL: CR→LF 변환
 *    - INLCR: LF→CR 변환
 *    - IUCLC: 대문자→소문자 변환
 *
 * 2. ISIG가 설정된 경우:
 *    - VINTR(^C)  → SIGINT
 *    - VQUIT(^\)  → SIGQUIT
 *    - VSUSP(^Z)  → SIGTSTP
 *    - VDSUSP(^Y) → SIGTSTP (delayed)
 *
 * 3. ICANON이 설정된 경우:
 *    - VERASE(^?) → 마지막 문자 삭제
 *    - VKILL(^U)  → 줄 전체 삭제
 *    - VWERASE(^W) → 마지막 단어 삭제
 *    - VEOF(^D)   → read() 반환 (빈 줄이면 EOF)
 *    - VEOL/VEOL2 → 줄 종료
 *    - '\n'       → 줄 종료 + 버퍼 → read() 전달
 *
 * 4. ECHO가 설정된 경우:
 *    - 일반 문자: 그대로 출력
 *    - 제어 문자: ^X 형식으로 출력
 *    - ECHOE: 백스페이스 시 "BS SP BS" 시퀀스
 *    - ECHOCTL: 제어 문자 ^X 표시
 *    - ECHOKE: kill 문자 시 전체 줄 지우기
 */

세션, 프로세스 그룹, 제어 터미널

TTY 서브시스템은 단순한 I/O를 넘어 프로세스 제어(Job Control)의 핵심 역할을 합니다. 로그인 셸은 세션 리더(session leader)로서 제어 터미널을 획득하고, foreground/background 프로세스 그룹을 관리합니다. 제어 터미널이 끊어지면(hangup) 세션 내 모든 프로세스에 SIGHUP이 전달됩니다.

제어 터미널 (/dev/pts/0) tty->session = sid, tty->pgrp = fg_pgid Session (SID = PID of session leader) Session Leader (bash) setsid() → 새 세션 생성 Foreground Process Group cat file.txt stdin ← TTY grep "foo" pipe 연결 ^C → SIGINT (fg group 전체) ^Z → SIGTSTP (fg group 전체) ^\ → SIGQUIT (fg group 전체) Background Process Group make & TTY 읽기 시 SIGTTIN sleep 100 & 백그라운드 TTY read() 시도 → SIGTTIN TTY write() (TOSTOP) → SIGTTOU
/* 세션/프로세스 그룹 관련 커널 구조 */

/* task_struct → signal_struct에서 세션/그룹 추적 */
struct signal_struct {
    struct pid *leader_pid;   /* 세션 리더 PID */
    struct pid *tty_old_pgrp; /* orphan pgrp SIGHUP용 */
    struct tty_struct *tty;   /* 제어 터미널 */
    /* ... */
};

/* tty_struct에서 세션/그룹 추적 */
struct tty_struct {
    struct pid *session;  /* 이 TTY를 소유한 세션 */
    struct pid *pgrp;     /* foreground 프로세스 그룹 */
    /* ... */
};

/* Hangup 시퀀스 (DCD 손실, USB 분리, kill -HUP):
 *
 * 1. tty_vhangup() 또는 tty_hangup() 호출
 * 2. __tty_hangup() 실행:
 *    a. ldisc->flush_buffer() — 버퍼 비우기
 *    b. driver->hangup() — 하드웨어 정리
 *    c. 세션의 모든 프로세스에 SIGHUP 전송
 *    d. 세션의 모든 프로세스에 SIGCONT 전송 (stopped면)
 *    e. tty 참조 해제
 *
 * 3. 세션 리더 종료 시:
 *    a. foreground pgrp에 SIGHUP + SIGCONT
 *    b. disassociate_ctty() → 제어 터미널 해제
 */

/* user space에서 세션/터미널 관리 */
/* setsid()      — 새 세션 생성 (제어 터미널 없음) */
/* ioctl(fd, TIOCSCTTY, 0) — 제어 터미널 획득 */
/* ioctl(fd, TIOCNOTTY)    — 제어 터미널 포기 */
/* tcsetpgrp(fd, pgrp)     — foreground 그룹 변경 */
/* tcgetpgrp(fd)           — foreground 그룹 조회 */
# 세션/프로세스 그룹 확인
ps -eo pid,pgid,sid,tty,comm | head -20
#   PID  PGID   SID TT       COMMAND
#     1     1     1 ?        systemd
#  1234  1234  1234 pts/0    bash       ← 세션 리더
#  5678  5678  1234 pts/0    vim        ← fg 그룹
#  9012  9012  1234 pts/0    make       ← bg 그룹

# nohup — 세션 분리 (SIGHUP 무시)
nohup long_running_cmd &
# SIGHUP 수신 시에도 프로세스 유지
# stdout → nohup.out 리디렉션

# disown — bash 내장 (job table에서 제거)
long_running_cmd &
disown %1
# bash 종료 시 SIGHUP 전송 대상에서 제거

RS-232 신호와 커넥터

RS-232(EIA-232)는 DTE(Data Terminal Equipment, 예: PC)와 DCE(Data Communication Equipment, 예: 모뎀) 사이의 직렬 통신 표준입니다. 전압 레벨(-3V~-15V = logic 1, +3V~+15V = logic 0), 커넥터 핀 배치, 제어 신호를 규정합니다.

DTE (PC/컴퓨터) DCE (모뎀) Pin 1: DCD ← DCD (Carrier Detect) Pin 2: RXD ← TXD → (DCE 송신) Pin 3: TXD → ← RXD (DCE 수신) Pin 4: DTR → ← DTR (단말기 준비) Pin 5: GND — — GND (신호 접지) Pin 6: DSR ← DSR (데이터셋 준비) Pin 7: RTS → ← RTS (송신 요청) Pin 8: CTS ← CTS (송신 허가) Pin 9: RI ← RI (Ring Indicator) 데이터 TX 데이터 RX 제어 신호
신호DB-9 핀방향Linux 상수역할
TXD3DTE→DCE-송신 데이터
RXD2DCE→DTE-수신 데이터
RTS7DTE→DCETIOCM_RTS송신 요청 (HW 흐름 제어 시 "수신 가능" 의미로도 사용)
CTS8DCE→DTETIOCM_CTS송신 허가 (DCE가 수신 준비 완료)
DTR4DTE→DCETIOCM_DTR단말기 준비 (open 시 활성화, close 시 비활성화)
DSR6DCE→DTETIOCM_DSR데이터셋 준비 (모뎀 전원 켜짐)
DCD1DCE→DTETIOCM_CAR캐리어 감지 (통신 연결 수립됨, CLOCAL 해제 시 의미 있음)
RI9DCE→DTETIOCM_RNG링 표시 (전화 벨 울림)
GND5--신호 접지 (공통 기준점)
Null Modem 케이블: DTE↔DTE 직접 연결 시 사용합니다. TXD↔RXD 교차, RTS↔CTS 교차, DTR→DCD+DSR 루프백으로 구성됩니다. 3선식 null modem은 TXD, RXD, GND만 사용하고 나머지 신호를 자기 쪽에서 루프백합니다. 리눅스에서는 CLOCAL 플래그를 설정하면 DCD 없이도 포트를 열 수 있습니다.

흐름 제어 메커니즘

흐름 제어(Flow Control)는 수신측이 처리 속도보다 빠르게 데이터가 도착할 때 데이터 손실을 방지하는 메커니즘입니다. 하드웨어(RTS/CTS)와 소프트웨어(XON/XOFF) 두 가지 방식이 있으며, 환경에 따라 선택합니다.

Hardware Flow Control (RTS/CTS) 송신측 수신측 RTS=1 CTS=1 (수신 가능) DATA → DATA → 버퍼 가득! CTS=0 (정지!) 대기... 버퍼 여유 CTS=1 (재개) DATA → CRTSCTS 플래그 전용 핀 사용 → 바이너리 데이터 안전 지연: 수 us (하드웨어 레벨) Software Flow Control (XON/XOFF) 송신측 수신측 DATA → DATA → DATA → 버퍼 가득! ← XOFF (0x13, ^S) 대기... 버퍼 여유 ← XON (0x11, ^Q) DATA → IXON/IXOFF 플래그 데이터 라인 사용 → 3선 가능 주의: 0x11/0x13이 데이터에 포함 시 문제
항목Hardware (RTS/CTS)Software (XON/XOFF)
설정stty crtsctsstty ixon ixoff
필요 배선TXD, RXD, RTS, CTS, GND (5선)TXD, RXD, GND (3선)
바이너리 안전예 (제어 신호가 별도 핀)아니오 (0x11, 0x13이 예약됨)
반응 속도매우 빠름 (하드웨어 레벨)느림 (데이터 경로로 전달)
커널 처리uart_ops->set_mctrl()port->x_char 메커니즘
N_TTY 관련throttle()/unthrottle()N_TTY가 XON/XOFF 자동 처리
적합 용도고속 통신, 바이너리 전송텍스트 터미널, 레거시 장비
/* 커널에서 throttle/unthrottle 처리 (drivers/tty/tty_ioctl.c) */

/* N_TTY read_buf가 가득 차면 호출 */
void tty_throttle(struct tty_struct *tty)
{
    mutex_lock(&tty->throttle_mutex);
    if (!tty_throttled(tty)) {
        if (tty->ops->throttle)
            tty->ops->throttle(tty);  /* → UART 드라이버 */
        set_bit(TTY_THROTTLED, &tty->flags);
    }
    mutex_unlock(&tty->throttle_mutex);
}

/* UART 드라이버의 throttle 구현 예 */
static void my_throttle(struct tty_struct *tty)
{
    struct uart_port *port = ...;

    if (C_CRTSCTS(tty)) {
        /* HW 흐름 제어: RTS 비활성화 → DCE가 송신 중지 */
        u32 mcr = readl(port->membase + REG_MCR);
        mcr &= ~MCR_RTS;
        writel(mcr, port->membase + REG_MCR);
    }
    if (I_IXOFF(tty)) {
        /* SW 흐름 제어: XOFF(0x13) 전송 */
        port->x_char = STOP_CHAR(tty);  /* 다음 TX ISR에서 전송 */
        my_start_tx(port);
    }
}

DMA 기반 UART 전송

고속 시리얼 통신(1Mbps 이상)에서는 PIO(Programmed I/O, 바이트 단위 ISR 처리)가 CPU 부하의 병목이 됩니다. DMA(Direct Memory Access)를 사용하면 CPU 개입 없이 대량의 데이터를 메모리↔UART 간에 직접 전송할 수 있습니다.

DMA RX: UART → DMA Engine → Flip Buffer UART RX FIFO DMA 트리거 DMA Engine dmaengine_submit() DMA RX Buffer dma_alloc_coherent Flip Buffer → N_TTY → user DMA TX: xmit_fifo → DMA Engine → UART TX FIFO xmit_fifo kfifo TX 버퍼 DMA Engine SG list 전송 UART TX FIFO 하드웨어 전송 TX 핀 출력 → RS-232 라인 PIO vs DMA 비교 PIO: 바이트마다 ISR → CPU 부하 ∝ 보레이트 DMA: 버퍼 단위 완료 인터럽트 → CPU 거의 유휴 115200 bps: PIO 충분 1Mbps+: DMA 필수
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>

/* DMA RX 설정 — circular buffer + 타이머 기반 */
static int my_uart_dma_rx_setup(struct my_uart_priv *priv)
{
    struct dma_slave_config cfg = {
        .direction    = DMA_DEV_TO_MEM,
        .src_addr     = priv->phys_base + REG_DATA,
        .src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE,
        .src_maxburst = 16,  /* FIFO 트리거 레벨과 일치 */
    };

    priv->rx_chan = dma_request_chan(&priv->pdev->dev, "rx");
    if (IS_ERR(priv->rx_chan))
        return PTR_ERR(priv->rx_chan);

    dmaengine_slave_config(priv->rx_chan, &cfg);

    /* DMA 버퍼 할당 (coherent — cache 동기화 불필요) */
    priv->rx_buf = dma_alloc_coherent(&priv->pdev->dev,
                                       RX_DMA_BUF_SIZE,
                                       &priv->rx_dma_addr,
                                       GFP_KERNEL);

    /* Circular DMA descriptor 준비 */
    priv->rx_desc = dmaengine_prep_dma_cyclic(
        priv->rx_chan,
        priv->rx_dma_addr,
        RX_DMA_BUF_SIZE,
        RX_DMA_BUF_SIZE / 2,  /* period: 반 버퍼마다 콜백 */
        DMA_DEV_TO_MEM,
        DMA_PREP_INTERRUPT);

    priv->rx_desc->callback = my_dma_rx_callback;
    priv->rx_desc->callback_param = priv;

    dmaengine_submit(priv->rx_desc);
    dma_async_issue_pending(priv->rx_chan);

    /* idle 타이머: DMA period 미달 시 잔여 데이터 처리 */
    timer_setup(&priv->rx_timer, my_dma_rx_timer, 0);

    return 0;
}

/* DMA RX 콜백 — period 완료 시 호출 */
static void my_dma_rx_callback(void *param)
{
    struct my_uart_priv *priv = param;
    struct tty_port *tport = &priv->port.state->port;
    size_t count;

    /* DMA 위치에서 새로 수신된 데이터량 계산 */
    struct dma_tx_state state;
    dmaengine_tx_status(priv->rx_chan, priv->rx_cookie, &state);
    count = RX_DMA_BUF_SIZE - state.residue - priv->rx_pos;

    if (count > 0) {
        tty_insert_flip_string(tport,
                               priv->rx_buf + priv->rx_pos, count);
        tty_flip_buffer_push(tport);
        priv->rx_pos = (priv->rx_pos + count) % RX_DMA_BUF_SIZE;
    }

    /* idle 타이머 리셋 */
    mod_timer(&priv->rx_timer, jiffies + msecs_to_jiffies(10));
}

/* DMA TX — scatter-gather 전송 */
static void my_dma_start_tx(struct my_uart_priv *priv)
{
    struct tty_port *tport = &priv->port.state->port;
    struct scatterlist sg[2];
    unsigned char *tail;
    size_t count;

    /* kfifo에서 연속 영역 얻기 (wrap-around 시 2개 SG) */
    count = kfifo_dma_out_prepare(&tport->xmit_fifo, sg, 2,
                                   kfifo_len(&tport->xmit_fifo));
    if (count == 0)
        return;

    priv->tx_desc = dmaengine_prep_slave_sg(
        priv->tx_chan, sg, count, DMA_MEM_TO_DEV,
        DMA_PREP_INTERRUPT);

    priv->tx_desc->callback = my_dma_tx_callback;
    priv->tx_desc->callback_param = priv;

    dmaengine_submit(priv->tx_desc);
    dma_async_issue_pending(priv->tx_chan);
}
DMA UART 주의사항:
  • idle 타이머 — DMA는 period 크기만큼 차야 콜백이 옵니다. 마지막 소량의 데이터가 DMA 버퍼에 남아 전달되지 않는 문제가 있으므로, UART idle interrupt 또는 타이머로 잔여 데이터를 처리해야 합니다.
  • cache coherencydma_alloc_coherent()를 사용하거나, dma_map_single() + dma_sync_*_for_cpu()로 캐시를 명시적으로 동기화해야 합니다.
  • 에러 처리 — DMA 전송 중 UART 에러(parity, frame, overrun)가 발생하면 DMA를 중지하고 PIO로 폴백하는 경로가 필요합니다.
  • suspend — PM suspend 시 진행 중인 DMA를 dmaengine_terminate_sync()로 안전하게 중지해야 합니다.

Virtual Console (VT)

가상 콘솔(VT, Virtual Terminal)은 물리 키보드/디스플레이에 연결된 텍스트 모드 터미널입니다. /dev/tty1~/dev/tty63으로 접근하며, Alt+F1~F6(또는 Ctrl+Alt+F1~F6)으로 전환합니다. VT 콘솔은 프레임버퍼(fbcon) 또는 DRM(drmfb) 위에서 텍스트를 렌더링합니다.

키보드 HW input subsystem keyboard.c keycode→keysym N_TTY ldisc VT Core (vt.c) vc_data 구조체 ANSI escape 파서 스크린 버퍼 관리 tty1 tty2 ... Console Driver fbcon / drmfb Framebuffer /dev/fb0 (fbdev) DRM/KMS i915, amdgpu 디스플레이 HW
# VT 콘솔 관련 명령
chvt 2                  # tty2로 전환 (Alt+F2와 동일)
fgconsole               # 현재 foreground VT 번호
deallocvt               # 미사용 VT 해제
setfont /usr/share/consolefonts/Lat2-Terminus16.psf  # 콘솔 폰트 변경

# VT 개수 제한 (커널 파라미터)
# CONFIG_NR_TTY_DEVICES=63 (기본 최대)

# VT 콘솔에 메시지 출력
echo "Hello VT" > /dev/tty1

# 커널 콘솔 로그 레벨 조정
dmesg -n 4              # WARNING 이상만 콘솔에 출력
echo 8 > /proc/sys/kernel/printk  # 모든 레벨 출력

virtio-console과 Serial over LAN

가상화 환경과 원격 서버 관리에서는 물리 시리얼 포트 없이 시리얼 인터페이스를 제공해야 합니다. virtio-console은 KVM/QEMU 게스트에서 호스트와의 고속 시리얼 통신을, Serial over LAN (SOL)은 BMC/IPMI를 통한 원격 콘솔 접근을 제공합니다.

기술사용 환경디바이스드라이버특징
virtio-consoleKVM/QEMU 게스트/dev/hvc0virtio_console.cvirtio 링 기반, 멀티포트, 고속
virtio-serialKVM/QEMU 게스트/dev/vport0p1virtio_console.c범용 데이터 채널 (에이전트 통신용)
IPMI SOL원격 서버 (BMC)/dev/ttyS0 (리다이렉트)BIOS/BMC 펌웨어네트워크 경유, IPMI 2.0 표준
iLO VSPHPE 서버가상 시리얼iLO 펌웨어iLO 웹/SSH 접근
DRACDell 서버가상 시리얼iDRAC 펌웨어racadm 명령
Xen consoleXen 하이퍼바이저/dev/hvc0hvc_xen.cXen 공유 링
# QEMU에서 virtio-console 설정
qemu-system-x86_64 \
  -device virtio-serial \
  -chardev socket,path=/tmp/console.sock,server=on,wait=off,id=char0 \
  -device virtconsole,chardev=char0,name=console \
  -kernel vmlinuz -append "console=hvc0"

# 호스트에서 접근
socat - UNIX-CONNECT:/tmp/console.sock

# IPMI SOL 접근
ipmitool -I lanplus -H bmc-ip -U admin -P pass sol activate
# 서버 커널 파라미터: console=ttyS0,115200n8
# BIOS에서 COM1 → SOL 리다이렉션 설정 필요

# virtio-console 멀티포트 (QEMU agent 통신용)
qemu-system-x86_64 \
  -device virtio-serial \
  -chardev socket,path=/tmp/qga.sock,server=on,wait=off,id=qga0 \
  -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0
# 게스트 내: /dev/virtio-ports/org.qemu.guest_agent.0

Device Tree UART 바인딩

임베디드 ARM/RISC-V 시스템에서 UART 포트는 Device Tree를 통해 기술됩니다. DT 바인딩은 포트의 물리 주소, 인터럽트, 클럭, DMA 채널, RS-485 설정 등을 정의합니다.

/* Device Tree UART 바인딩 예시 — 종합 */

/* 1. 기본 UART 노드 */
uart0: serial@1c28000 {
    compatible = "allwinner,sun50i-a64-uart",
                 "snps,dw-apb-uart";  /* fallback */
    reg = <0x01c28000 0x400>;  /* MMIO 영역 */
    reg-shift = <2>;           /* 레지스터 간격: 4바이트 */
    reg-io-width = <4>;        /* 32-bit 접근 */
    interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&ccu CLK_BUS_UART0>;
    resets = <&ccu RST_BUS_UART0>;
    clock-frequency = <24000000>;  /* uartclk (없으면 clocks에서 추출) */
    status = "okay";

    /* 핀 멀티플렉싱 */
    pinctrl-names = "default";
    pinctrl-0 = <&uart0_pins>;

    /* DMA 채널 바인딩 */
    dmas = <&dma 6>, <&dma 6>;  /* TX, RX */
    dma-names = "tx", "rx";
};

/* 2. RS-485 설정 */
uart1: serial@1c28400 {
    compatible = "ti,am654-uart";
    /* ... 기본 속성 ... */

    linux,rs485-enabled-at-boot-time;
    rs485-rts-delay = <0 0>;   /* before_send, after_send (ms) */
    rs485-rts-active-low;       /* RTS 극성 반전 */
    rs485-rx-during-tx;         /* full-duplex RS-485 */
};

/* 3. serdev 자식 노드 (Bluetooth) */
uart2: serial@1c28800 {
    compatible = "allwinner,sun50i-a64-uart";
    /* ... 기본 속성 ... */

    bluetooth {
        compatible = "realtek,rtl8723bs-bt";
        max-speed = <1500000>;    /* 최대 보레이트 */
        enable-gpios = <&pio 6 18 GPIO_ACTIVE_HIGH>;
        device-wake-gpios = <&pio 6 17 GPIO_ACTIVE_HIGH>;
    };
};

/* 4. 시리얼 alias (포트 번호 고정) */
aliases {
    serial0 = &uart0;  /* /dev/ttyS0 */
    serial1 = &uart1;  /* /dev/ttyS1 */
    serial2 = &uart2;  /* /dev/ttyS2 */
};

/* 5. stdout-path (earlycon/콘솔 자동 감지) */
chosen {
    stdout-path = "serial0:115200n8";
    /* 형식: "serial별명:보레이트{패리티}{데이터비트}{흐름제어}" */
    /* 예: "serial0:115200n8r" (r=RTS/CTS) */
};

전원 관리 — Suspend/Resume/Wakeup

UART 드라이버의 전원 관리는 시스템 suspend(S3/S4)와 runtime PM 두 차원에서 이루어집니다. 시리얼 포트가 wakeup 소스로 설정되면 suspend 상태에서도 수신 데이터가 시스템을 깨울 수 있습니다.

System Suspend → Resume 시퀀스 uart_suspend _port() TX drain FIFO 비우기 DMA 중지 terminate_sync IRQ 해제 disable_irq clk 비활성화 clk_disable SUSPENDED Wakeup IRQ (RX 수신 시) enable_irq_wake(port->irq) RESUME clk 활성화 clk_enable 레지스터 복원 LCR, IER, MCR DMA 재시작 cyclic RX 재제출 IRQ 활성화 enable_irq uart_resume _port() Runtime PM: 포트 미사용 시 자동으로 clk 비활성화 pm_runtime_get_sync() / pm_runtime_put_autosuspend() autosuspend_delay = 500ms (보통) — open/close 기반 또는 TX/RX 기반
/* UART PM 구현 핵심 */

/* System suspend */
static int my_uart_suspend(struct device *dev)
{
    struct my_uart_priv *priv = dev_get_drvdata(dev);

    /* serial_core가 TX drain, ldisc flush 등을 처리 */
    uart_suspend_port(&my_uart_drv, &priv->port);

    /* DMA 중지 */
    if (priv->rx_chan)
        dmaengine_terminate_sync(priv->rx_chan);
    if (priv->tx_chan)
        dmaengine_terminate_sync(priv->tx_chan);

    /* wakeup 소스 설정 */
    if (device_may_wakeup(dev))
        enable_irq_wake(priv->port.irq);

    /* 클럭 비활성화 */
    clk_disable_unprepare(priv->clk);

    return 0;
}

/* System resume */
static int my_uart_resume(struct device *dev)
{
    struct my_uart_priv *priv = dev_get_drvdata(dev);

    clk_prepare_enable(priv->clk);

    if (device_may_wakeup(dev))
        disable_irq_wake(priv->port.irq);

    /* 레지스터 복원은 uart_resume_port() 내부에서
     * set_termios()를 재호출하여 자동 처리 */
    uart_resume_port(&my_uart_drv, &priv->port);

    /* DMA 재시작 */
    if (priv->rx_chan)
        my_uart_dma_rx_setup(priv);

    return 0;
}

/* Runtime PM 콜백 */
static int my_uart_runtime_suspend(struct device *dev)
{
    struct my_uart_priv *priv = dev_get_drvdata(dev);
    clk_disable_unprepare(priv->clk);
    return 0;
}

static int my_uart_runtime_resume(struct device *dev)
{
    struct my_uart_priv *priv = dev_get_drvdata(dev);
    return clk_prepare_enable(priv->clk);
}

static const struct dev_pm_ops my_uart_pm_ops = {
    SET_SYSTEM_SLEEP_PM_OPS(my_uart_suspend, my_uart_resume)
    SET_RUNTIME_PM_OPS(my_uart_runtime_suspend,
                       my_uart_runtime_resume, NULL)
};

TTY 보안 — SAK와 접근 제어

Secure Attention Key (SAK)는 TTY에 연결된 모든 프로세스를 강제 종료하여, 가짜 로그인 프로그램(trojan login)으로부터 보호하는 보안 메커니즘입니다. SAK를 누르면 해당 TTY의 모든 프로세스가 종료되고, 진짜 login 프로그램만 남게 됩니다.

보안 메커니즘설명설정
SAKTTY의 모든 프로세스 종료 (Alt+SysRq+K)echo 1 > /proc/sys/kernel/sak
TTY 소유권로그인 시 /dev/ttyN 소유권을 사용자에게 변경login / systemd-logind
PTY 분리devpts 네임스페이스로 컨테이너 간 PTY 격리mount -t devpts -o newinstance
TIOCSTI 차단다른 TTY에 키 입력 주입 방지커널 5.2+ TIOCSTI 제한
TIOCLINUX 제한VT 콘솔 복사/붙여넣기 제어CAP_SYS_ADMIN 필요
read/write 권한/dev/ttyS* 그룹 dialoutusermod -aG dialout user
/* SAK 처리 (drivers/tty/tty_io.c) */
void __do_SAK(struct tty_struct *tty)
{
    struct task_struct *p;

    /* 1. tty의 세션 내 모든 프로세스에 SIGKILL */
    do_each_pid_task(tty->session, PIDTYPE_SID, p) {
        send_sig(SIGKILL, p, 1);
    } while_each_pid_task(tty->session, PIDTYPE_SID, p);

    /* 2. 이 tty의 fd를 열고 있는 모든 프로세스에도 SIGKILL */
    for_each_process(p) {
        if (task_has_tty_fd(p, tty))
            send_sig(SIGKILL, p, 1);
    }

    /* 3. tty를 초기 상태로 리셋 */
    tty_reset_termios(tty);
}

/* TIOCSTI: 다른 TTY에 키 입력 주입 (보안 위험)
 *
 * 커널 6.2+에서 제한:
 * - sysctl kernel.tiocsti_restrict = 1 (기본 비활성화)
 * - 또는 CONFIG_LEGACY_TIOCSTI=n
 * - CAP_SYS_ADMIN 없이는 TIOCSTI 사용 불가
 *
 * 공격 시나리오:
 * 1. 공격자가 victim의 /dev/pts/N에 접근
 * 2. ioctl(fd, TIOCSTI, 'r') — 'r' 키 입력 주입
 * 3. 연쇄 호출로 "rm -rf /" 등의 명령 실행 가능
 */

성능 최적화 — 고속 UART 튜닝

시리얼 통신의 성능 병목은 보레이트만이 아닙니다. 인터럽트 빈도, FIFO 트리거 레벨, DMA 활용, N_TTY 오버헤드, 프로세스 스케줄링 지연 등 여러 계층에서 최적화할 수 있습니다.

최적화 포인트기본값튜닝효과
RX FIFO 트리거1바이트 (트리거 즉시)FIFO 절반 (8~64)인터럽트 횟수 대폭 감소
DMA 사용PIODMA cyclic RX + SG TXCPU 부하 95%+ 감소 (고속 시)
low_latency0setserial /dev/ttyS0 low_latencyflip buffer → 즉시 ldisc 전달 (workqueue 우회)
N_TTY bypassN_TTY 사용커스텀 ldisc 또는 serdevN_TTY 오버헤드 제거
커스텀 보레이트표준 (115200 등)BOTHER + c_ospeed비표준 보레이트 (예: 250000, 500000)
RT 스케줄링CFS시리얼 처리 스레드 RT 우선순위worst-case 지연 개선
IRQ affinity자동echo N > /proc/irq/IRQ/smp_affinityUART IRQ를 전용 코어에 고정
/* 비표준 보레이트 설정 (user space) */
#include <asm/termbits.h>
#include <sys/ioctl.h>

struct termios2 tio;
ioctl(fd, TCGETS2, &tio);

tio.c_cflag &= ~CBAUD;
tio.c_cflag |= BOTHER;         /* 비표준 보레이트 사용 */
tio.c_ispeed = 250000;          /* 250Kbps */
tio.c_ospeed = 250000;

ioctl(fd, TCSETS2, &tio);

/* latency_timer 조정 (FTDI USB-Serial) */
/* /sys/bus/usb-serial/devices/ttyUSB0/latency_timer
 * 기본값: 16ms → 1ms로 줄이면 지연 개선 (USB 마이크로프레임 기반)
 */
echo 1 > /sys/bus/usb-serial/devices/ttyUSB0/latency_timer
# 성능 측정 도구

# 시리얼 처리량 측정
dd if=/dev/urandom bs=1024 count=1000 | pv > /dev/ttyS0
# 수신측: pv < /dev/ttyS0 > /dev/null

# 왕복 지연(latency) 측정
# 간단한 에코 서버로 RTT 측정
stty -F /dev/ttyS0 4000000 raw -echo
# 타임스탬프 기반 RTT 측정 프로그램 사용

# 인터럽트 빈도 모니터링
watch -n1 'cat /proc/interrupts | grep serial'

# UART 오버런 에러 확인
cat /proc/tty/driver/serial
# 0: ... oe:5 (overrun error 5회 발생)

흔한 실수와 트러블슈팅

실수 1: DTR/RTS 미처리로 포트 열림 실패

일부 장비는 DTR이 활성화되어야 통신을 시작합니다. CLOCAL을 설정하지 않으면 open()이 DCD 대기로 블로킹됩니다.

stty -F /dev/ttyS0 clocal  # DCD 무시
# 또는 O_NDELAY / O_NONBLOCK으로 open
실수 2: 바이너리 데이터에 XON/XOFF 활성 상태

소프트웨어 흐름 제어가 켜진 상태에서 바이너리 데이터를 전송하면, 0x11(XON)과 0x13(XOFF) 바이트가 제어 문자로 해석되어 데이터가 훼손됩니다.

stty -F /dev/ttyS0 -ixon -ixoff  # 소프트웨어 흐름 제어 해제
# 바이너리 전송 시 raw 모드 사용 필수
실수 3: N_TTY가 CR/LF를 변환

기본 설정에서 ICRNL(입력 CR→LF)과 ONLCR(출력 LF→CRLF)가 활성화되어 바이너리 데이터가 변형됩니다.

stty -F /dev/ttyS0 raw  # 모든 가공 비활성화
# 또는 cfmakeraw()로 프로그래밍적 설정
실수 4: USB-Serial 디바이스 이름 변동

/dev/ttyUSB0은 연결 순서에 따라 바뀝니다. udev 규칙으로 고정 symlink를 생성하세요.

# /etc/udev/rules.d/99-serial.rules
SUBSYSTEM=="tty", ATTRS{serial}=="AB0123CD", SYMLINK+="ttyGPS"
# 이후 /dev/ttyGPS로 안정적 접근
실수 5: ISR에서 mutex/sleep 호출

UART 인터럽트 핸들러는 hard IRQ 컨텍스트입니다. mutex_lock(), msleep(), kmalloc(GFP_KERNEL) 등 슬립 가능 함수를 호출하면 커널 패닉이 발생합니다. spin_lock()GFP_ATOMIC만 사용하세요.

실수 6: console_write()에서 데드락

printk()가 UART 콘솔을 통해 출력될 때, UART 드라이버 내부에서 printk()를 호출하면 재귀 → 데드락이 발생합니다. console_write() 경로에서는 dev_dbg()/printk() 사용을 피하세요.

실전 실습

실습 환경: QEMU + virtio-serial, 또는 USB-Serial 변환기 2개를 null modem 연결, 또는 socat으로 가상 시리얼 페어를 생성하여 실습할 수 있습니다.

Lab 1: socat으로 가상 시리얼 페어 생성

# 가상 시리얼 포트 쌍 생성
socat -d -d pty,raw,echo=0,link=/tmp/ttyV0 pty,raw,echo=0,link=/tmp/ttyV1

# 터미널 1: 수신
cat /tmp/ttyV0

# 터미널 2: 송신
echo "Hello Serial!" > /tmp/ttyV1

# minicom으로 양방향 통신
minicom -D /tmp/ttyV0 -b 115200   # 터미널 1
minicom -D /tmp/ttyV1 -b 115200   # 터미널 2

Lab 2: stty로 termios 실험

# 현재 설정 저장
stty -g -F /tmp/ttyV0 > /tmp/stty_backup

# canonical 모드 테스트
stty -F /tmp/ttyV0 icanon echo
# → 줄 단위 입력, 에코 활성, ^C/^Z 동작

# raw 모드 테스트
stty -F /tmp/ttyV0 raw
# → 바이트 즉시 전달, 에코 없음, 시그널 없음

# VMIN/VTIME 실험
stty -F /tmp/ttyV0 -icanon min 5 time 20
# → 5바이트 수신 또는 2초 타임아웃 시 read() 반환

# 설정 복원
stty -F /tmp/ttyV0 $(cat /tmp/stty_backup)

Lab 3: 커스텀 Line Discipline 테스트

# SLIP line discipline으로 시리얼 위 IP 통신

# 호스트 A (ttyS0):
slattach -p slip -s 115200 /dev/ttyS0 &
ifconfig sl0 10.0.0.1 pointopoint 10.0.0.2 up

# 호스트 B (ttyS0):
slattach -p slip -s 115200 /dev/ttyS0 &
ifconfig sl0 10.0.0.2 pointopoint 10.0.0.1 up

# 핑 테스트
ping 10.0.0.2
# 시리얼 라인 위로 IP 패킷이 전송됩니다

Lab 4: RS-485 Half-Duplex 통신

/* RS-485 모드로 Modbus RTU 통신 실습 */
#include <linux/serial.h>
#include <sys/ioctl.h>

int fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY);

/* RS-485 모드 활성화 */
struct serial_rs485 rs485;
memset(&rs485, 0, sizeof(rs485));
rs485.flags = SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND;
rs485.delay_rts_before_send = 1;  /* 1ms */
rs485.delay_rts_after_send  = 1;
ioctl(fd, TIOCSRS485, &rs485);

/* 9600 8N1 설정 */
struct termios tio;
tcgetattr(fd, &tio);
cfmakeraw(&tio);
cfsetspeed(&tio, B9600);
tio.c_cflag |= CLOCAL | CREAD;
tio.c_cc[VMIN] = 0;
tio.c_cc[VTIME] = 10;  /* 1초 타임아웃 */
tcsetattr(fd, TCSANOW, &tio);

/* Modbus RTU 프레임 송신 */
uint8_t req[] = {0x01, 0x03, 0x00, 0x00,
                  0x00, 0x0A, 0xC5, 0xCD};
write(fd, req, sizeof(req));
tcdrain(fd);  /* TX 완료 대기 (RTS 전환 전 필수!) */

/* 응답 수신 */
uint8_t resp[256];
int n = read(fd, resp, sizeof(resp));

Lab 5: /proc/tty 심층 분석

# 등록된 모든 TTY 드라이버 확인
cat /proc/tty/drivers
# driver_name      device_name   major  minor_range  type

# 활성 line discipline 목록
cat /proc/tty/ldiscs

# 시리얼 포트 상세 (인터럽트 카운터 포함)
cat /proc/tty/driver/serial
# 0: uart:16550A port:03F8 irq:4 tx:1234 rx:5678 oe:0 pe:0 fe:0 brk:0
#    tx: 송신 바이트, rx: 수신 바이트, oe: overrun, pe: parity error
#    fe: frame error, brk: break 수신

# sysfs로 UART 타입 확인
for d in /sys/class/tty/ttyS*; do
    echo "$(basename $d): type=$(cat $d/type) uartclk=$(cat $d/uartclk)"
done

# debugfs UART 상태 (일부 드라이버)
cat /sys/kernel/debug/serial/ttyS0
# 또는
cat /sys/kernel/debug/8250/ttyS0

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