인터럽트 & UART 드라이버

sungho·2025년 1월 2일

디바이스 드라이버

목록 보기
8/12

임베디드 시스템에서 실시간으로 이벤트를 처리하는 기술은 인터럽트입니다. UART 통신은 인터럽트를 활용하여 데이터를 주고받는 역할을 수행합니다. 인터럽트 처리 과정과 UART 드라이버의 작동 원리 그리고 리눅스 커널에서의 구현 사례를 알아보도록 하겠습니다.

1. 인터럽트 개념

  • 인터럽트란 OS에게 디바이스의 상태 변화를 알리는 신호
    • 키보드의 키가 눌리거나 네트워크 패킷이 도착하거나 타이머가 만료되는 등의 이벤트가 발생하면 해당 디바이스는 인터럽트를 발생시켜 OS에게 알림
  • 인터럽트의 특징은 비동기적 처리
    • 인터럽트가 발생하면 프로세스는 현재 수행 중인 작업을 잠시 중단하고 인터럽트 핸들러를 실행하여 해당 이벤트를 처리
  • 인터럽트 핸들러는 인터럽트를 처리하는 함수
    • 커널 내부 디바이스 드라이버 내에서 동작
    • 인터럽트 핸들러는 제한된 환경에서 작동
      • Sleep 함수를 사용할 수 없으며 빠르게 처리 완료가 필요
  • 인터럽트 발생 지점은 하드웨어 플랫폼에 따라 다름
    • 라즈베리 파이에서는 특정 GPIO 핀에 신호가 입력되거나 내장된 타이머가 만료될 때 인터럽트가 발생

인터럽트 발생부터 처리까지의 과정

  1. 인터럽트 발생
    • 디바이스에서 인터럽트 신호 발생
  2. 인터럽트 전달
    • 인터럽트 컨트롤러를 통해 프로세서에 전달
  3. 현재 상태 저장
    • 프로세스는 현재 수행 중인 작업의 상태를 저장(PC -> ELR_EL1, PSTATE -> SPSR_EL1)
  4. 인터럽트 핸들러 실행
    • ASM IRQ 핸들러가 인터럽트 종류를 파악하고 인터럽트 핸들러를 호출(PC -> LR)
  5. 인터럽트 처리
    • C 서브루틴에서 인터럽트 처리
  6. 이전 상태 복원
    • 저장된 상태를 복원하여 중단되었던 작업을 재개(LR -> PC, SPSR_EL1 -> PSTATE)

디바이스 드라이버 구성

  • Top half
    • 인터럽트 컨텍스트에서 동작
      • 제한된 환경에서 빠르게 동작
  • Bottom half
    • 스레드 컨텍스트에서 동작
    • Sleep 함수나 blocking lock을 사용
    • Top half에서 처리하기에는 시간이 오래 걸리거나 복잡한 작업을 Bottom half에서 처리

2. 인터럽트 특징

인터럽트

  • 빠른 응답 속도
  • 디바이스에서 이벤트가 발생하면 즉각적으로 처리
    • 기가비트 이더넷은 초당 1.5 million의 패킷을 처리할 수 있으며 1 microsecond마다 인터럽트가 발생
  • 빈번한 인터럽트는 CPU 오버헤드를 증가
    • save/restore 작업과 cache misses를 빈번하게 발생시켜 시스템 성능을 저하

폴링

  • 주기적으로 디바이스의 상태를 확인
    • 인터럽트가 발생할 때까지 기다리지 않고 CPU가 직접 디바이스의 상태를 주기적으로 검사
    • 빈번한 인터럽트 발생을 방지하여 오버헤드를 감소시키는 장점이 있지만 느린 디바이스에서는 CPU 사이클을 낭비하는 단점이 존재

비교분석

  • 고속 디바이스(이더넷)

    • 폴링이 효율적
      • 빈번한 인터럽트로 인한 오버헤드보다 주기적인 폴링으로 인한 오버헤드가 적기 때문
  • 저속 디바이스(키보드, I2C, UART)

    • 인터럽트가 효율적
      • 이벤트 발생 빈도가 낮기 때문에 폴링으로 인한 CPU 사이클 낭비가 인터럽트 처리 오버헤드보다 클 수 있음
  • 리눅스 커널은 장단점을 고려하여 저속 디바이스에는 인터럽트를 사용하고 고속 디바이스에는 인터럽트와 폴링을 함께 사용하는 하이브리드 방식을 채택

    • 고속 디바이스의 경우 트래픽 상황에 따라 동적으로 인터럽트와 폴링을 전환하는 NAPI(New API) 방식을 사용하여 최적의 성능을 제공(Low rate일 때는 인터럽트, High rate일 때는 폴링으로 전환)
  • 동적 전환(rdc/r6040.c)

static int r6040_poll(struct napi_struct *napi, int budget)
{
	struct r6040_private *priv =
		container_of(napi, struct r6040_private, napi);
	struct net_device *dev = priv->dev;
	void __iomem *ioaddr = priv->base;
	int work_done;

	r6040_tx(dev);

	work_done = r6040_rx(dev, budget);

	if (work_done < budget) {
		napi_complete_done(napi, work_done);
		/* Enable RX/TX interrupt */
		iowrite16(ioread16(ioaddr + MIER) | RX_INTS | TX_INTS,
			  ioaddr + MIER);
	}
	return work_done;
}

3. 인터럽트 종류

  • IRQ 번호를 할당받으며 OS는 번호를 통해 어떤 디바이스에서 인터럽트가 발생했는지 식별

  • IRQ 번호는 하드웨어 플랫폼에서 정의

  • 라즈베리 파이 4의 인터럽트 맵을 살펴보면 주변 장치에 할당된 IRQ 번호를 확인

    • Qemu와 실제 RPI4 보드의 IRQ 번호가 다를 수 있음
  • 인터럽트를 사용하기 위해서는 먼저 OS에 해당 인터럽트를 등록

  • 리눅스 커널에서는 request_threaded_irq 함수를 사용하여 인터럽트를 등록

int request_threaded_irq(unsigned int irq,
                         irq_handler_t handler,
                         irq_handler_t thread_fn,
                         unsigned long irqflags,
                         const char *devname,
                         void *dev_id);
  • irq
    • 등록할 인터럽트의 IRQ 번호. platform_get_irq() 함수를 사용
  • handler
    • 인터럽트 발생 시 호출될 인터럽트 핸들러(Top half) 함수
  • thread_fn
    • 인터럽트 스레드 (Bottom half) 함수
    • IRQF_ONESHOT 플래그 사용 시 이 함수는 별도의 스레드에서 실행
  • irqflags
    • 인터럽트의 속성을 지정하는 플래그
      • IRQF_SHARED
        • 여러 디바이스가 동일한 인터럽트를 공유할 수 있도록 허용
      • IRQF_ONESHOT
        • threaded 인터럽트를 사용하도록 지정
  • devname
    • 인터럽트를 요청하는 디바이스의 이름
  • dev_id
    • 인터럽트를 요청하는 디바이스의 고유 식별자
    • request_irq가 free_irq와 한 쌍으로 사용되는 경우 free_irq 호출 시 dev_id를 사용하여 어떤 장치의 irq를 free할지 식별
    • dev 인자는 devm_request_threaded_irq 사용 시에만 필요

4. GIC-400 인터럽트 컨트롤러

  • GIC-400은 ARM 프로세서에서 사용되는 인터럽트 컨트롤러
    • 우선순위에 따라 프로세서 코어에 전달하는 기능
  • GIC-400은 PPI, SPI, SGI와 같은 다양한 종류의 인터럽트를 처리
    • PPI
      • 특정 코어에만 전달되는 인터럽트
        • 타이머와 같이 특정 코어에 종속적인 장치에서 사용
    • SPI
      • 여러 코어에 전달될 수 있는 인터럽트
        • I2C, UART와 같이 여러 코어가 공유하는 장치에서 사용
    • SGI
      • 소프트웨어적으로 발생하는 인터럽트
        • 코어 간 통신에 사용
  • GIC-400은 인터럽트 라우팅 기능을 제공하여 인터럽트 소스로부터 발생한 인터럽트를 프로세서 코어로 전달
    • 인터럽트 우선순위를 관리하여 중요한 인터럽트가 먼저 처리

5. UART 통신 및 PL011 UART 드라이버

  • UART는 비동기 직렬 통신 방식으로 두 장치 간에 데이터를 직렬로 전송
    • 디버깅 및 콘솔 용도
  • TTY(Teletypewriter)는 터미널 장치를 추상화하는 리눅스 서브시스템
    • TTY 서브시스템은 사용자 공간 프로그램에게 일관된 인터페이스를 제공
  • PL011은 ARM에서 널리 사용되는 UART 컨트롤러
    • 리눅스 UART 드라이버는 PL011 UART 컨트롤러를 제어하고 TTY 서브시스템과 연동하여 사용자에게 직렬 포트를 제공

PL011 UART 컨트롤러는 다양한 레지스터를 통해 제어

  • UARTDR
    • 데이터를 송수신하는 데 사용되는 레지스터
  • UARTRSR/UARTECR
    • 수신 상태 및 에러를 확인하고 클리어하는 데 사용되는 레지스터
  • UARTFR
    • UART의 상태를 나타내는 플래그 레지스터
  • UARTILPR
    • IrDA 저전력 모드 카운터 레지스터
  • UARTIBRD/UARTFBRD
    • 보드 레이트를 설정하는 레지스터
  • UARTLCR_H
    • 데이터 비트, 정지 비트, 패리티 비트 등을 설정하는 라인 제어 레지스터
  • UARTCR
    • UART 활성화, 송수신 활성화 등을 설정하는 제어 레지스터
  • UARTIFLS
    • 인터럽트 FIFO 레벨을 설정하는 레지스터
  • UARTIMSC
    • 인터럽트 마스크를 설정/클리어하는 레지스터
  • UARTRIS/UARTMIS
    • 원시/마스킹된 인터럽트 상태를 나타내는 레지스터
  • UARTICR
    • 인터럽트 클리어 레지스터
  • UARTDMACR
    • DMA (Direct Memory Access)를 제어하는 레지스터

6. 콘솔 키보드 입력 처리과정

  • Qemu 콘솔은 console=ttyAMA0,115200으로 설정
  • ttyAMA0 장치를 통해 콘솔 입출력이 이루어짐
  • ttyAMA0은 PL011 UART 드라이버에 의해 제어되는 직렬 포트

사용자가 콘솔에 'a'를 입력하면 다음과 같은 과정

  1. 인터럽트 발생
    • PL011 UART 컨트롤러에서 수신 인터럽트 발생
  2. 인터럽트 핸들러 호출
    • PL011 UART 드라이버의 인터럽트 핸들러(pl011_int)가 호출됨
  3. 데이터 읽기
    • pl011_rx_chars 함수가 UART FIFO에서 문자 'a'를 읽음
  4. TTY 레이어로 전달
    • pl011_fifo_to_tty 함수가 읽은 문자 'a'를 TTY 레이어로 전달
  5. 화면 출력
    • TTY 드라이버는 문자 'a'를 화면에 출력

// amba-pl011.c
// ...

static irqreturn_t pl011_int(int irq, void *dev_id)
{
    struct uart_amba_port *uap = dev_id;
    unsigned long flags;
    unsigned int status, pass_counter = AMBA_ISR_PASS_LIMIT;
    int handled = 0;

    spin_lock_irqsave(&uap->port.lock, flags);

    status = pl011_read(uap, REG_RIS) & uap->im;

    do {
        if (status & (UART011_RTIS | UART011_RXIS)) {

            if (pl011_dma_rx_running(uap))
                pl011_rx_irq(uap);
            else
                pl011_rx_chars(uap);
        }
// ...
        if (pass_counter-- == 0)
            break;

        status = pl011_read(uap, REG_RIS) & uap->im;
    } while (status & (UART011_TXIS | UART011_RTIS | UART011_RXIS));

    spin_unlock_irqrestore(&uap->port.lock, flags);

    return IRQ_RETVAL(handled);
}

// ...

static void pl011_rx_chars(struct uart_amba_port *uap)
{
    struct tty_port *port = &uap->port.state->port;
    unsigned int status, ch, flag, fifotaken;

    while (!pl011_tx_stopped(uap) &&
           (fifotaken = pl011_read_raw_fifo(uap, &ch, &status, &flag))) {
        uap->port.icount.rx++;
        uart_handle_sysrq_char(&uap->port, ch);

        if (uart_handle_break(&uap->port))
            continue;

        tty_insert_flip_char(port, ch, flag);
    }

    spin_unlock(&uap->port.lock);
    tty_flip_buffer_push(port);
    spin_lock(&uap->port.lock);
}

// ...

static int pl011_startup(struct uart_port *port)
{
    struct uart_amba_port *uap =
        container_of(port, struct uart_amba_port, port);
    unsigned int cr;
    int retval;

    retval = pl011_hwnit(port);
    if (retval)
        goto clk_dis;

    retval = pl011_allocate_irq(uap);
    if (retval)
        goto clk_dis;

// ...
    return 0;

clk_dis:
    clk_disable_unprepare(uap->clk);
    return retval;
}

static int pl011_allocate_irq(struct uart_amba_port *uap)
{
    int ret;

    pl011_write(uap, 0, REG_IMSC);

    ret = request_irq(uap->port.irq, pl011_int, IRQF_SHARED,
              "uart-pl011", uap);
    if (ret)
        dev_err(uap->port.dev,
            "Failed to register AMBA-PL011 interrupt\n");

    return ret;
}

static int pl011_probe(struct amba_device *dev, const struct amba_id *id)
{
    // ...
    ret = pl011_setup_port(dev, &uap->port, &res, portnr);
    if (ret)
        goto unregister_region;

    pl011_setup_fifos(uap);

    uap->port.dev = &dev->dev;
    uap->port.irq = dev->irq[0];
    uap->port.ops = &amba_pl011_pops;

    // ...
    pl011_register_port(uap);

    return ret;
}

static struct amba_driver pl011_driver = {
    // ...
    .probe = pl011_probe,
    // ...
};

static int __init pl011_init(void)
{
    printk(KERN_INFO "Serial: AMBA PL011 UART driver\n");

    return amba_driver_register(&pl011_driver);
}

// ...

module_init(pl011_init);
  • pl011_startup()

    • UART 포트를 초기화하는 함수
      • pl011_hwnit()
        • 하드웨어를 초기화
      • pl011_allocate_irq()
        • 인터럽트를 할당
        • request_irq를 호출하여 pl011_int 함수를 인터럽트 핸들러로 등록
  • pl011_rx_chars()

    • 수신 인터럽트를 처리하는 Top half 함수
      • *pl011_read_raw_fifo 함수를 사용하여 UART FIFO에서 데이터 리딩
      • pl011_fifo_to_tty()
        • 읽은 데이터를 TTY 레이어로 전달
  • pl011_int()

    • 인터럽트 핸들러(Top half) 함수
      • pl011_rx_chars()
        • 수신 인터럽트 처리 함수를 호출
  • pl011_probe()

    • 드라이버 초기화 함수
      • pl011_register_port()
        • UART 포트를 시스템에 등록
  • pl011_init()

    • 모듈 초기화 함수
      • amba_driver_register()
        • AMBA 버스에 드라이버를 등록
  • initcall은 빌드인(builtin) 드라이버의 초기화 순서를 정의하는 메커니즘

    • pl011_driver는 device_initcall로 등록되어 부팅 시 자동으로 초기화
// init/main.c
// ...

#define core_initcall(fn)        __define_initcall(fn, 1)
#define postcore_initcall(fn)    __define_initcall(fn, 2)
#define arch_initcall(fn)        __define_initcall(fn, 3)
#define subsys_initcall(fn)      __define_initcall(fn, 4)
#define fs_initcall(fn)          __define_initcall(fn, 5)
#define device_initcall(fn)      __define_initcall(fn, 6)
#define late_initcall(fn)        __define_initcall(fn, 7)
  • initcall에는 core_initcall, postcore_initcall, arch_initcall 등 다양한 레벨이 있으며 숫자가 작을수록 먼저 초기화

7. 결론

인터럽트 처리 과정, UART 드라이버의 작동 원리 그리고 리눅스 커널에서의 구현 사례를 자세히 살펴보았습니다. 인터럽트는 임베디드 시스템에서 발생하는 이벤트를 신속하고 효율적으로 처리하는 기술이며 UART는 인터럽트를 활용하여 데이터를 주고받는 역할을 수행합니다.

0개의 댓글