Serial / TTY 서브시스템
Serial/TTY 서브시스템을 콘솔, 제어 채널, 산업 통신까지 포함한 입출력 관점에서 심층 분석합니다. UART 하드웨어와 tty core 계층 분리, tty_driver/tty_port 구조, line discipline과 termios 파라미터 적용, RX/TX 버퍼링과 플로우 제어, console/earlycon 부팅 로그 경로, DMA 기반 고속 UART 처리, hangup·재연결·오류 플래그 복구, setserial/stty/debugfs 기반 디버깅까지 현장 장비 운용에 필요한 실전 포인트를 다룹니다.
핵심 요약
- 초기화 순서 — 탐색, 바인딩, 자원 등록 순서를 점검합니다.
- 제어/데이터 분리 — 빠른 경로와 설정 경로를 분리 설계합니다.
- IRQ/작업 분할 — 즉시 처리와 지연 처리를 구분합니다.
- 안전 한계 — 전원/열/타이밍 임계값을 함께 관리합니다.
- 운영 복구 — 오류 시 재초기화와 롤백 경로를 준비합니다.
단계별 이해
- 장치 수명주기 확인
probe부터 remove까지 흐름을 점검합니다. - 비동기 경로 설계
IRQ, 워크큐, 타이머 역할을 분리합니다. - 자원 정합성 검증
DMA/클록/전원 참조를 교차 확인합니다. - 현장 조건 테스트
연결 끊김/복구/부하 상황을 재현합니다.
Serial / TTY 서브시스템
TTY(Teletypewriter) 서브시스템은 리눅스 커널에서 가장 오래되고 복잡한 계층 중 하나입니다. 원래 물리적 텔레타이프 단말기를 위해 설계되었지만, 현대 리눅스에서는 시리얼 포트, 가상 콘솔, 의사 터미널(PTY), USB 시리얼 등 다양한 문자 기반 I/O 인터페이스를 통합 관리합니다.
TTY 코어 데이터 구조
TTY 서브시스템의 핵심 구조체들은 include/linux/tty.h와 include/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_TTY | 0 | 기본 터미널 I/O (canonical/raw 모드) | drivers/tty/n_tty.c |
| N_SLIP | 1 | Serial Line IP — 시리얼 위 IP 통신 | drivers/net/slip/ |
| N_PPP | 3 | Point-to-Point Protocol | drivers/net/ppp/ |
| N_GSM0710 | 21 | GSM 멀티플렉싱 (모뎀) | drivers/tty/n_gsm.c |
| N_NULL | 27 | 모든 데이터를 버림 (테스트용) | drivers/tty/n_null.c |
| N_TRACESINK | 23 | 디버그 트레이스 데이터 싱크 | 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_port와 uart_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/ttyS0 | 8250/16550 호환 시리얼 포트 | drivers/tty/serial/8250/ |
| ttyAMAN | /dev/ttyAMA0 | ARM AMBA PL011 UART | drivers/tty/serial/amba-pl011.c |
| ttyUSBN | /dev/ttyUSB0 | USB-Serial 변환기 (FTDI, CP210x 등) | drivers/usb/serial/ |
| ttyACMN | /dev/ttyACM0 | USB CDC ACM (Abstract Control Model) | drivers/usb/class/cdc-acm.c |
| ttyMFDN | /dev/ttyMFD0 | Intel MID (Medfield) UART | drivers/tty/serial/mfd.c |
| ttyON | /dev/ttyO0 | TI OMAP UART | drivers/tty/serial/omap-serial.c |
| ttySACN | /dev/ttySAC0 | Samsung S3C/S5P UART | drivers/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/ptmx | PTY master 멀티플렉서 | drivers/tty/pty.c |
| console | /dev/console | 시스템 콘솔 (마지막 console= 파라미터) | 커널 코어 |
| ttyGSN | /dev/ttyGS0 | USB Gadget 시리얼 (디바이스 모드) | drivers/usb/gadget/function/u_serial.c |
| ttyLPN | /dev/ttyLP0 | Intel 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의 입력으로 나타나고, 그 반대도 마찬가지입니다.
/* 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
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);
}
- 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/resume —
uart_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/THR | 0x00 | 수신 데이터 (RBR) | 송신 데이터 (THR) |
| IER | 0x01 | 인터럽트 활성화 (RX, TX, Line Status, Modem Status) | |
| IIR/FCR | 0x02 | 인터럽트 식별 (IIR) | FIFO 제어 (FCR) |
| LCR | 0x03 | Line Control (데이터 비트, 정지 비트, 패리티, DLAB) | |
| MCR | 0x04 | Modem Control (DTR, RTS, loopback) | |
| LSR | 0x05 | Line Status (Data Ready, Overrun, Parity Err, TX Empty) | |
| MSR | 0x06 | Modem Status (CTS, DSR, RI, DCD 변화 감지) | |
| SCR | 0x07 | Scratch Register (UART 존재 감지용) | |
| DLL/DLM | 0x00/0x01 | Divisor 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
*/
| 서브시스템 | 주요 드라이버 | 디버깅 도구 |
|---|---|---|
| Input | gpio-keys, atkbd, hid-* | evtest, libinput debug-events |
| USB | xhci-hcd, ehci-hcd, usb-storage | lsusb -v, usbmon |
| V4L2 | uvcvideo, vivid | v4l2-ctl, media-ctl |
| DRM | i915, amdgpu, nouveau | modetest, drm_info |
| ALSA | snd-hda-intel, snd-usb-audio | aplay -l, alsamixer |
| Serial | 8250, pl011, imx-uart | minicom, stty |
관련 문서
이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.