Linux Serial/TTY 서브시스템: UART 드라이버, tty_driver 구조, Line Discipline, termios 설정, 시리얼 포트 관리, 커널 콘솔 종합 가이드
이 개념을 정확히 이해하려면 'TTY'와 'Serial'이라는 두 가지 요소를 나누어서 살펴보는 것이 좋습니다.
TTY는 Teletypewriter(텔레타이프라이터)의 약자입니다.
- 역사적 배경: 초기 컴퓨터 시대에는 모니터와 키보드가 없었고, 타자기처럼 생긴 기계(텔레타이프라이터)를 컴퓨터에 연결해 타자를 치면 종이에 결과가 인쇄되는 방식으로 컴퓨터와 통신했습니다.
- 현재의 의미: 오늘날 물리적인 타자기는 사라졌지만, 리눅스/유닉스 운영체제는 텍스트를 입력받고 출력하는 모든 표준 환경(터미널 창, 콘솔 등)을 여전히 'TTY'라고 부릅니다.
Serial TTY(시리얼 TTY)는 유닉스 및 리눅스 시스템에서 직렬 통신(Serial Communication)을 통해 장치에 접속하고 텍스트 기반으로 명령을 내보낼 수 있게 해주는 터미널 인터페이스를 의미합니다. 데이터를 한 번에 한 비트씩 순차적으로 전송하는 통신 방식이며, 흔히 알고 있는 RS-232 케이블이나 보드에 내장된 UART 통신이 여기에 해당합니다.
이 두 가지를 합친 Serial TTY는 모니터나 키보드, 네트워크(LAN/Wi-Fi) 연결 없이 순수하게 직렬 통신 케이블(Serial Cable)만을 연결하여 대상 기기의 터미널(쉘)에 접속하는 방식을 말합니다.
왜 아직도 널리 사용할까요? 주요 활용처는?
최첨단 시대에도 Serial TTY는 개발자나 엔지니어에게 '최후의 보루' 역할을 합니다.
- 임베디드 시스템 및 IoT 개발: 라즈베리파이(Raspberry Pi), 아두이노, 개발용 보드 등 모니터를 직접 연결하기 힘든(Headless) 장비의 초기 설정과 디버깅에 필수적입니다.
- 네트워크 장비 제어: 시스코(Cisco) 라우터나 스위치 같은 엔터프라이즈 네트워크 장비는 기본적으로 전용 콘솔 포트(Serial TTY)를 통해 IP 설정 및 관리를 진행합니다.
- 서버 복구 (Kernel Panic): 네트워크 설정이 잘못되어 SSH 접속이 끊어지거나 운영체제 커널에 심각한 오류가 발생했을 때, Serial TTY로 연결하면 시스템의 부팅 로그나 에러 메시지를 가장 로우레벨에서 확인할 수 있습니다.
■ 리눅스에서의 Serial TTY 디바이스 파일
리눅스 시스템에 접속해보면 /dev/ 디렉토리 아래에 여러 종류의 TTY 파일들이 존재합니다. 연결 방식에 따라 이름이 다릅니다.
- /dev/ttyS0, /dev/ttyS1: 전통적인 PC의 직렬 포트 (COM1, COM2)
- /dev/ttyUSB0, /dev/ttyUSB1: USB to Serial 변환 케이블을 사용해 연결했을 때 생성되는 포트 (가장 흔하게 볼 수 있음)
- /dev/ttyAMA0 또는 /dev/ttyO2: ARM 기반 아키텍처(라즈베리파이, 비글본 등)의 하드웨어 직렬 포트
■ Mac에서의 Serial 디바이스 파일
Mac(macOS)은 유닉스(BSD)를 뿌리로 두고 있기 때문에 리눅스와 전반적인 개념은 매우 비슷합니다. 하지만 Serial TTY 디바이스 파일을 다루는 방식에서 리눅스와 결정적인 차이점 하나가 있습니다.
바로 하나의 물리적인 시리얼 포트에 대해 tty.*와 cu.*라는 두 개의 디바이스 파일이 동시에 생성된다는 점입니다.
Mac 환경에서 시리얼 통신을 원활하게 하기 위해 꼭 알아두어야 할 특징들을 정리해 드립니다.
5.1 Mac에서 시리얼 장치 확인하는 법
Mac에 USB to Serial 케이블이나 장비를 연결한 후, 터미널(Terminal)을 열고 아래 명령어를 입력하면 연결된 장치 목록을 확인할 수 있습니다.
ls -l /dev/tty.* /dev/cu.*
5.2 가장 중요한 차이: tty vs cu
이 부분이 Mac에서 시리얼 통신을 할 때 많은 분들이 헷갈려하는 핵심입니다. 장치를 연결하면 /dev/tty.usbserial-xxx와 /dev/cu.usbserial-xxx가 쌍으로 생성되는데, 용도가 다릅니다.
| 구분 | 파일명 예시 | 연결 방향 | 작동 방식 (DCD 신호 대기 여부) |
| TTY | /dev/tty.usbserial-... | 수신 (Incoming) | 상대방 장치에서 먼저 신호(Carrier)를 보낼 때까지 대기(Block)합니다. |
| CU | /dev/cu.usbserial-... | 발신 (Outgoing) | 'Call Up'의 약자입니다. 상대방 신호를 기다리지 않고 즉시 연결을 시도합니다. |
💡 핵심 팁: 라즈베리파이, 아두이노, 네트워크 라우터 등에 Mac에서 먼저 접속하여 명령을 내리려는 경우, 반드시 /dev/cu.* 디바이스를 사용해야 합니다. > /dev/tty.*로 접속을 시도하면 상대방이 신호를 보낼 때까지 화면이 멈춘 것처럼 먹통(Hang)이 되는 현상을 겪게 됩니다.
5.3 대표적인 디바이스 파일명 패턴
사용하는 USB to Serial 변환 칩셋(FTDI, CP210x, CH340 등)이나 드라이버에 따라 생성되는 이름이 조금씩 다릅니다.
- /dev/cu.usbserial-1410 등: FTDI 칩셋이나 일반적인 USB-Serial 변환기를 꽂았을 때 가장 흔하게 나타나는 이름입니다. (뒤의 숫자는 Mac의 USB 포트 위치에 따라 달라집니다.)
- /dev/cu.SLAB_USBtoUART: 실리콘랩스(Silicon Labs)의 CP210x 계열 칩셋을 사용하는 보드(예: ESP32 등)를 연결했을 때 자주 보입니다.
- /dev/cu.usbmodem...: 아두이노(Arduino) 계열 보드처럼 시리얼 통신 기능이 내장된 USB 장치를 꽂았을 때 주로 나타납니다.
- /dev/cu.Bluetooth-Incoming-Port: 블루투스 시리얼 프로토콜을 위한 포트로, 항상 기본적으로 존재하지만 케이블 연결 시에는 무시하셔도 됩니다.
Mac 환경에서 /dev/cu.* 장치를 찾으셨다면, 이제 터미널 프로그램을 이용해 실제로 장비에 접속할 차례입니다. Mac에 기본 내장된 screen 명령어나 별도의 프로그램(예: Serial, minicom)을 활용해 직접 장비에 접속하는 구체적인 명령어나 설정 방법을 안내해 드릴까요?
■ 접속 시 필요한 통신 규약
Serial TTY로 장비에 접속하려면 양쪽 기기의 통신 설정이 정확히 일치해야 글자가 깨지지 않습니다. 보통 다음의 설정을 맞춥니다.
- Baud Rate (보드레이트): 통신 속도. 보통 115200 이나 9600 bps를 가장 많이 사용합니다.
- Data bits / Parity / Stop bits: 대체로 8N1 (8개의 데이터 비트, 패리티 없음, 1개의 스톱 비트) 설정을 표준으로 사용합니다.
※ Serial / TTY 서브시스템
전제 조건: 디바이스 드라이버와 Workqueue를 먼저 학습하세요. 입출력 인터페이스 드라이버는 데이터 경로와 제어 경로를 동시에 다루므로 큐/버퍼/비동기 처리 경계를 먼저 구분해야 합니다.
일상 비유: 이 주제는 콜센터 접수와 처리 라인 분리가 비슷합니다. 요청 접수와 실제 처리를 분리해 병목을 줄이듯, 드라이버도 IRQ·큐·작업 스레드를 역할별로 나눠야 안정적입니다.
핵심 요약
- 초기화 순서 — 탐색, 바인딩, 자원 등록 순서를 점검
- 제어/데이터 분리 — 빠른 경로와 설정 경로를 분리 설계
- IRQ/작업 분할 — 즉시 처리와 지연 처리를 구분
- 안전 한계 — 전원/열/타이밍 임계값을 함께 관리
- 운영 복구 — 오류 시 재초기화와 롤백 경로를 준비
단계별 이해
① 장치 수명 주기 확인
probe부터 remove까지 흐름을 점검
② 비동기 경로 설계
IRQ, 워크큐, 타이머 역할을 분리
③ 자원 정합성 검증
DMA/클록/전원 참조를 교차 확인
④ 현장 조건 테스트
연결 끊김/복구/부하 상황을 재현
※ 관련 표준: RS-232C, RS-485, POSIX termios - 시리얼 통신 및 터미널 제어 표준입니다.
※ 관련 페이지: 디바이스 드라이버, 커널 버스 서브시스템
1. Serial / TTY 서브 시스템
TTY(Teletypewriter) 서브시스템은 리눅스 커널에서 가장 오래되고 복잡한 계층 중 하나입니다. 원래 물리적 텔레타이프 단말기를 위해 설계되었지만, 현대 리눅스에서는 시리얼 포트, 가상 콘솔, 의사 터미널(PTY), USB 시리얼 등 다양한 문자 기반 I/O 인터페이스를 통합 관리합니다.

2. TTY 코어 데이터 구조
TTY 서브시스템의 핵심 구조체들은 include/linux/tty.h 와 include/linux/tty_driver.h 에 정의되어 있습니다. 각 구조체의 역할과 관계를 이해하는 것이 TTY/Serial 드라이버 개발의 시작입니다.
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; /* 콘솔 포트 여부 */
/* ... */
};
3. 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);
/* ... */
};
4. Flip Buffer : 수신 데이터 경로
인터럽트 핸들러에서 수신된 데이터를 user space로 전달하는 메커니즘입니다. ISR에서 직접 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로 데이터 전달
*/
5. 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 변경
*/
6. serial_core 프레임워크 : UART 드라이버
serial_core ( driver/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);
7. 시리얼 콘솔과 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에서 자동 감지
8. 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 |
9. 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;
}
10. 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
11. 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 극성 반전
* };
*/
12. 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 컨텍스트 — 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 대기, 클럭 재설정 등 순서 준수 필수
13. 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 |
'Project ES > : : Peripheral' 카테고리의 다른 글
| ADI - DAI와 NXP - SAI (0) | 2026.05.07 |
|---|---|
| PIO와 Pin Muxing (0) | 2026.03.23 |
| A2B 오디오 버스: 10년과 3세대의 역사 (1) | 2026.02.16 |
| R&D Stories: Automotive Audio Bus 시작하기 (4부) (0) | 2026.02.16 |
| R&D Stories: Automotive Audio Bus 시작하기 (3부) (0) | 2026.02.16 |