Bottom Half & GPIO 인터럽트

sungho·2025년 1월 5일

디바이스 드라이버

목록 보기
9/12

1. 인터럽트의 Top Half와 Bottom Half

  • 리눅스 커널에서 인터럽트 처리는 Top Half와 Bottom Half라는 두 단계로 나누어 처리
    • 이유는 빠른 처리와 효율적인 자원 관리를 위해

1.1 Top Half(빠르게 최소한의 작업)

  • 인터럽트 컨텍스트에서 동작
    • 인터럽트가 발생하면 즉시 실행되어 최대한 빠르게 처리
  • 제한된 환경
    • sleep 함수나 mutex와 같은 blocking lock 사용이 불가능
      • 오랫동안 인터럽트 컨텍스트에 머무르면 시스템 전체 성능에 악영향
  • 기능
    • Acknowledge to device
      • 인터럽트 발생을 알리고 다음 인터럽트를 받을 수 있도록 하드웨어를 설정
    • 최소한의 작업 수행
      • 레지스터 읽기/쓰기와 같이 간단하고 빠르게 처리해야 하는 작업만 수행
    • Bottom Half 깨우기
      • wake_up_interruptible()과 같은 함수를 사용하여 대기 중인 Bottom Half를 깨우기

1.2 Bottom Half(느긋하게 복잡한 작업)

  • 스레드(일반적으로 커널 스레드) 컨텍스트에서 동작
    • Top Half에서 깨워주면 실행
    • sleep 함수나 blocking lock 사용이 가능
  • 시간이 오래 걸리는 작업 처리
    • Top Half에서 처리하기 어려웠던 복잡한 데이터 처리, 파일 시스템 접근, 네트워크 통신 등 시간이 오래 걸리는 작업들을 수행

2. Bottom Half 구현

2.1 Kthread + Wait Queue 코드

#define BUF_SIZE 1024
static char kernel_write_buffer[BUF_SIZE];
static dev_t kdt_dev;
static struct cdev kdt_cdev;
static struct class *kdt_class;
static struct task_struct *wait_thread;
static int wait_queue_flag = 0;
DECLARE_WAIT_QUEUE_HEAD(wait_queue);
#define DRIVER_NAME "kdt_kthread_driver"
#define DRIVER_CLASS "kdt_kthread_class"
#define KDT_GPIO_OUTPUT 17 // 예시
#define KDT_GPIO_INPUT 16  // 예시// 유저 공간에서 read 시스템 콜 호출 시 실행
static ssize_t kdt_driver_read(struct file *filp, char __user *buf, size_t size, loff_t *offset) {
  if (copy_to_user(buf, "t", 1)) {
    pr_err("read: error\n");
    return -EFAULT;
  }

  pr_info("Read Function\n");
  wait_queue_flag = 1; // 대기 큐 플래그 설정
  wake_up_interruptible(&wait_queue); // 대기 중인 kthread 깨우기
  pr_info("Value of button: %d\n", gpio_get_value(KDT_GPIO_INPUT)); // GPIO 값 읽기 (예시)

  return BUF_SIZE; 
}

// kthread로 실행되는 Bottom Half 함수
static int kdt_wait_kthread(void *unused) {
  while (1) { 
    pr_info("waiting for event\n");
    wait_event_interruptible(wait_queue, wait_queue_flag != 0); // 이벤트 대기
    if (wait_queue_flag == 2) { 
      
      return 0; 
    }
    pr_info("wake-up!!!\n");
    wait_queue_flag = 0; // 대기 큐 플래그 초기화
  }
  do_exit(0);
  return 0;
}

// 모듈 초기화 함수
static int __init kdt_driver_init(void) {
  // ... (디바이스 등록 코드 생략) ...

  // kthread 생성
  wait_thread = kthread_create(kdt_wait_kthread, NULL, "kdt_wait_thread");
  if (wait_thread) {
    pr_info("Thread created successfully\n");
    wake_up_process(wait_thread); // 스레드 깨우기
  } else {
    pr_info("Thread creation failed\n");
    return -ENOMEM;
  }

  // ... (GPIO 설정 코드 생략) ...

  return 0;
}

// 모듈 종료 함수
static void __exit kdt_driver_exit(void) {
  // ... (디바이스 해제 코드 생략) ...
  // ... (GPIO 해제 코드 생략) ...
}

module_init(kdt_driver_init);
module_exit(kdt_driver_exit);
MODULE_LICENSE("GPL");

코드 설명

  1. kdt_driver_read
    • 유저 프로그램이 read 시스템 콜을 호출하면 실행
      • "t"라는 문자열을 유저 공간 버퍼에 복사
      • wait_queue_flag를 1로 설정하여 kdt_wait_kthread에게 이벤트를 알림
      • wake_up_interruptible(&wait_queue)를 호출하여 대기 중인 kdt_wait_kthread를 깨우기
      • gpio_get_value(KDT_GPIO_INPUT)를 호출하여 GPIO 핀의 값을 읽어 출력
  2. kdt_wait_kthread
    • 커널 스레드로 실행되는 Bottom Half 함수
      • wait_event_interruptible(wait_queue, wait_queue_flag != 0)을 호출하여 wait_queue_flag가 0이 아닐 때까지 대기
      • wait_queue_flag가 2이면 스레드를 종료
      • wait_queue_flag가 1이면 "wake-up!!!" 메시지를 출력하고 wait_queue_flag를 0으로 초기화
  3. kdt_driver_init
    • kthread_create 함수를 사용하여 kdt_wait_kthread 함수를 실행하는 커널 스레드를 생성
    • wake_up_process 함수를 사용하여 생성된 스레드를 깨우기

3. GPIO 인터럽트

3.1. 핵심 개념

  • GPIO(General Purpose Input/Output)
    • 범용 입출력 핀으로 외부 신호를 입력받거나 출력하는 데 사용
  • GPIO Input 설정
    • GPIO 핀을 입력으로 설정하여 외부 인터럽트로 사용
      • 버튼 입력, 센서 신호 등을 감지
  • 채터링(Chattering)
    • 기계식 스위치(버튼)에서 발생하는 현상으로 스위치가 on/off 될 때 짧은 시간 동안 여러 번 접촉/단락이 반복되는 현상
  • 디바운스(Debounce)
    • 채터링 현상을 방지하기 위한 기술

3.2. 커널 모듈 코드

#define SIGR 45 // 유저 공간에 전달할 시그널 번호#define BUF_SIZE 1024
static char kernel_write_buffer[BUF_SIZE];
static dev_t kdt_dev;
static struct cdev kdt_cdev;
static struct class *kdt_class;
static struct task_struct *wait_thread;
int wait_queue_flag = 0;
unsigned int button_irq;
static struct task_struct *user_space_task = NULL; // 유저 공간 태스크 구조체
DECLARE_WAIT_QUEUE_HEAD(wait_queue);
#define DRIVER_NAME "kdt_interrupt_driver"#define DRIVER_CLASS "kdt_interrupt_class"#define KDT_GPIO_OUTPUT 17 // 예시#define KDT_GPIO_INPUT 16

// 유저 공간에서 read 시스템 콜 호출 시 실행
static ssize_t kdt_driver_read(struct file *filp, char __user *buf, size_t size, loff_t *offset) {
  if (copy_to_user(buf, "t", 1)) {
    pr_err("read: error\n");
    return -EFAULT;
  }

  user_space_task = get_current(); // 시그널을 전달할 유저 공간 태스크 저장
  pr_info("Userspace PID: %d is registered\n", user_space_task->pid);
  wait_queue_flag = 3; // 대기 큐 플래그 설정
  wake_up_interruptible(&wait_queue); // 대기 중인 kthread 깨우기
  pr_info("Value of button: %d\n", gpio_get_value(KDT_GPIO_INPUT));

  return BUF_SIZE;
}

// kthread로 실행되는 Bottom Half 함수
static int kdt_wait_kthread(void *unused) {
 while (1) {
    pr_info("waiting for event\n");
    wait_event_interruptible(wait_queue, wait_queue_flag != 0);
    if (wait_queue_flag == 2) {
      
      return 0;
    }
    pr_info("wake-up!!!\n");
    wait_queue_flag = 0;
  }
  do_exit(0);
  return 0;
}

// GPIO 인터럽트 핸들러 (Bottom Half)
irqreturn_t kdt_gpio_irq_signal_handler(int irq, void *data) {
  pr_info("kdt_gpio_irq_signal_handler: interrupt triggered!!!\n");
  if (user_space_task) {
    send_sig(SIGR, user_space_task, 0); // 유저 공간 태스크에 시그널 전송
  }
  return IRQ_HANDLED;
}

// 모듈 초기화 함수
static int __init kdt_driver_init(void) {
  int ret;
  // ... (디바이스 등록 코드 생략) ...

  // kthread 생성
  wait_thread = kthread_create(kdt_wait_kthread, NULL, "kdt_wait_thread");
  if (wait_thread) {
    pr_info("Thread created successfully\n");
    wake_up_process(wait_thread); // 스레드 깨우기
  } else {
    pr_info("Thread creation failed\n");
    return -ENOMEM;
  }

  // GPIO 핀을 IRQ 번호로 변환
  button_irq = gpio_to_irq(KDT_GPIO_INPUT);

  // 디바운스 설정 (채터링 방지)
  gpio_set_debounce(KDT_GPIO_INPUT, 300);

  // 인터럽트 등록
  if (request_threaded_irq(button_irq, (irq_handler_t)kdt_gpio_irq_signal_handler, IRQF_TRIGGER_RISING, "kdt_gpio_irq_signal", NULL) != 0) {
    pr_err("Error! Can not request interrupt nr: %d\n", button_irq);
    gpio_free(KDT_GPIO_INPUT);
    return -1;
  }

  return 0;
}

// 모듈 종료 함수
static void __exit kdt_driver_exit(void) {
  // ... (디바이스 해제 코드 생략) ...
}

module_init(kdt_driver_init);
module_exit(kdt_driver_exit);
MODULE_LICENSE("GPL");

3.3. 유저 공간 애플리케이션 코드

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#define MODULE_FILENAME "/dev/kdt_interrupt_driver" // 커널 모듈 디바이스 파일#define SIGTX 45 // 사용할 시그널 번호// 시그널 핸들러 함수

void signalhandler(int sig) {
  printf("Button pressed!\n");
}

int main(int argc, char *argv[]) {
  int dev;
  char buff;

  // 시그널 핸들러 등록
  signal(SIGTX, signalhandler);
  printf("PID: %d\n", getpid());

  // 커널 모듈 디바이스 파일 열기
  dev = open(MODULE_FILENAME, O_RDWR | O_NDELAY);
  if (dev < 0) {
    printf("module open error\n");
    exit(1);
  }

  // 커널 모듈에 유저 공간 태스크 등록
  if (read(dev, &buff, 1) < 0) {
    printf("read error\n");
    goto err;
  }
  printf("read data: %c\n", buff);

  printf("Waiting for button press...\n");
  while (1) {
    sleep(1); // 1초 간격으로 대기
  }

err:
  close(dev);
  return 0;
}

3.4. 코드 설명

  1. 커널 모듈
    • kdt_driver_read
      • 유저 프로그램이 read를 호출하면 get_current()를 통해 유저 공간 태스크의 task_struct를 user_space_task에 저장
      • wait_queue_flag를 3으로 설정하고 wake_up_interruptible을 호출하여 kdt_wait_kthread를 깨우기
    • kdt_wait_kthread
      • wait_queue_flag가 3이 되면 "wake-up!!!" 메시지를 출력하고 wait_queue_flag를 0으로 초기화
        • wake_up_interruptible이 어떻게 다른 스레드를 깨우는지 보여주는 예시
    • kdt_gpio_irq_signal_handler
      • 인터럽트가 발생하면 실행
        • user_space_task가 NULL이 아니면 send_sig를 사용하여 저장된 유저 공간 태스크에 SIGR 시그널을 보내기
    • 모듈 초기화
      • kthread를 생성하고 IRQ 번호를 얻고 디바운스를 설정하고, 인터럽트(Threaded IRQ 가 아님 )를 등록
  2. 유저 공간 애플리케이션
    • signal 함수를 사용하여 SIGTX 시그널에 대한 핸들러(signalhandler)를 등록
    • open 함수로 커널 모듈의 디바이스 파일을 열기
    • read 시스템 콜을 호출하여 커널 모듈의 kdt_driver_read 함수를 실행
      • 자신의 task_struct를 커널 모듈에 등록
    • while 루프 안에서 sleep 함수를 사용하여 1초 간격으로 대기
    • 버튼이 눌리면 커널에서 send_sig 함수가 호출되고 유저 공간 애플리케이션의 signalhandler 함수가 실행되어 "Button pressed!" 메시지가 출력

4. 결론

지금까지 Bottom Half의 개념과 Kthread + Wait Queue를 사용한 구현 방법 그리고 GPIO 인터럽트를 활용한 실전 예제를 통해 인터럽트 처리에 대해 알아보았습니다.

0개의 댓글