이번 글에서는 PCIe 디바이스를 다루는 것보다 디바이스 드라이버 작성 방식에 대해 초점을 맞추도록 한다.
디바이스 드라이버 with 인터럽트
직전 글에서 사용한 코드를 약간 수정하여 디바이스 드라이버에서 인터럽트를 핸들링하는 방법을 알아보도록 하자.
2024.11.29 - [임베디드 개발/리눅스 디바이스 드라이버] - LDD ] PCIe 디바이스 드라이버 작성하기 - (2)
하드웨어 장치는 특정 작업을 완료하거나 상태가 변경될 때 CPU에 인터럽트를 발생시켜 처리를 요청하는데 이를 IRQ(Interrupt Request)라고 한다. 그리고 디바이스 드라이버는 인터럽트가 발생했을 때 호출될 IRQ 핸들러를 등록하여 인터럽트를 처리할 수 있다.
이번 목표는 PSR 레지스터를 타이머를 사용해서 폴링으로 체크하는게 아니라 장치가 발생시킨 IRQ를 감지하여 LED 출력을 토글시키는 것이다.
먼저 lspci 명령을 사용해 PCIe 카드 정보를 확인한 결과, 해당 장치는 IRQ 30번을 사용하며 인터럽트를 지원하는 것으로 나타난다. 참고로, 여기서 IRQ number는 시스템이 주변 장치의 인터럽트를 관리하기 위해 매핑한 숫자일 뿐, 실제 CPU와의 물리적 라인과는 관련이 없다.
다음으로 PSR 레지스터에서도 인터럽트 상태를 확인할 수 있다. !INTFLAG 비트 설명을 보면 ACK 입력 핀에서 라이징 엣지가 발생 시 인터럽트 플래그가 생성되고, 해당 비트는 0으로 설정되며, 이후 PSR 레지스터를 읽으면 1로 설정된다.
인터럽트를 핸들링하기 위한 주요 등록 / 해제 / 핸들링 함수의 원형과 사용법을 살펴보자.
1. request_irq
디바이스가 인터럽트를 발생시킬 때 호출될 IRQ 핸들러를 등록하는 함수이다.
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev_id);
- irq : 등록할 인터럽트 번호로 PCI 디바이스에서는 pdev->irq를 사용하여 IRQ 번호를 가져온다.
- handler : 인터럽트 발생 시 호출될 콜백 함수
- flags : 인터럽트 동작 방식을 정의하는 설정 플래그로 OR 연산을 사용해 여러 플래그를 동시에 설정할 수 있다.
- name : 인터럽트를 등록할 디바이스의 이름으로 /proc/interrupts/ 에서 목록을 확인할 수 있다.
- dev_id : 핸들러에 전달할 데이터의 주소. 대부분의 경우 디바이스 컨텍스트(상태 데이터)를 들고 있는 구조체 포인터를 넘긴다. 공유 인터럽트의 경우 dev_id를 통해 어떤 디바이스에서 IRQ가 발생했는지 구분할 수 있다.
✔️ Flags의 종류
- IRQF_SHARED :
- 여러 장치가 하나의 IRQ 라인을 공유하도록 설정하는 방식으로 인터럽트 라인을 절약하기 위해 자주 사용된다.
- 핸들러에서는 dev_id를 사용해 장치를 구분할 수 있다.
- 공유 인터럽트에서는 인터럽트 발생 시 등록된 모든 핸들러가 순차적으로 호출되며, 하나라도 IRQ_HANDLED를 반환하면 인터럽트가 처리된 것으로 간주된다.
- 핸들러에서는 자신의 장치에서 발생한 인터럽트가 아닌 경우 IRQ_NONE을 반환해 미처리 했음을 알린다.
- IRQF_ONESHOT :
- request_threaded_irq 함수를 사용하고 1차 핸들러와 2차 핸들러를 함께 등록한다.
- 1차 핸들러에서 IRQ_WAKE_THREAD를 반환하면 2차 핸들러가 스레드 컨텍스트에서 실행된다.
- 1차 핸들러는 해당 인터럽트 라인을 비활성화하여 다른 인터럽트가 발생하지 않도록 차단한다. 2차 핸들러는 완료 시 인터럽트를 다시 활성화 한다.
- 긴 작업을 인터럽트 컨텍스트에서 처리하면 시스템이 멈출 수 있으니, 빠르게 하드웨어 인터럽트를 처리한 후 추가 작업을 커널 스레드에서 수행하는 방식이다.
- IRQF_NO_THREAD :
- 인터럽트 핸들러가 스레드 컨텍스트로 변환되지 않도록 강제하는 플래그이다.
- 일반적인 상황에서 인터럽트 핸들러는 항상 하드웨어 컨텍스트에서 실행되므로, 보통 이 플래그를 사용할 일은 없다.
- 다만 모든 인터럽트를 강제로 스레드화하는 커널 옵션(CONFIG_FORCE_IRQ_THREADING)이 활성화 되어있는 경우 해당 플래그를 사용하여 하드웨어 컨텍스트에서만 실행되도록 강제할 수 있다.
- IRQF_DISABLED :
- 인터럽트 핸들러가 실행되는 동안, 해당 CPU에서 모든 인터럽트를 비활성화한다.
- 다른 인터럽트가 중첩되지 않도록 보호하지만 핸들러 실행 시간이 길 경우 시스템 성능 저하가 발생할 수 있다.
- 짧고 간단한 인터럽트에 사용하는 것이 권장된다.
- IRQF_TRIGGER_RISING :
- Rising Edge에서 인터럽트가 발생하도록 설정한다.
- IRQF_TRIGGER_FALLING :
- Falling Edge에서 인터럽트가 발생하도록 설정한다.
2. free_irq
더 이상 인터럽트를 처리하지 않도록 인터럽트 핸들러를 해제하는 함수이다.
void free_irq(unsigned int irq, void *dev_id);
- irq : 해제할 인터럽트 번호
- dev_id : request_irq 호출 시 넘겼던 데이터의 주소와 동일한 값을 넘긴다.
3. irq_handler_t
request_irq 함수를 통해 등록하는 인터럽트 핸들러 함수 포인터의 원형은 다음과 같다.
irqreturn_t (*irq_handler_t)(int irq, void *dev_id);
- irq : 인터럽트를 발생시킨 IRQ 번호
- dev_id : request_irq 호출 시 등록한 디바이스 컨텍스트(드라이버 관리 구조체)의 주소
✔️ irqreturn_t
irqreturn_t는 인터럽트 핸들러의 반환 타입으로 핸들러가 인터럽트를 처리했는지 여부를 나타낸다.
/**
* enum irqreturn - irqreturn type values
* @IRQ_NONE: interrupt was not from this device or was not handled
* @IRQ_HANDLED: interrupt was handled by this device
* @IRQ_WAKE_THREAD: handler requests to wake the handler thread
*/
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
- IRQ_NONE : 인터럽트를 처리하지 않았음을 의미하며, 공유 인터럽트에서 다른 핸들러에게 인터럽트를 넘긴다.
- IRQ_HANDLED : 인터럽트를 성공적으로 처리했음을 의미하며, 공유 인터럽트에서 다른 핸들러는 호출되지 않는다.
- IRQ_WAKE_THREAD : 스레드화된 핸들러를 깨울 때 사용하며, IRQF_ONESHOT 플래그와 함께 사용된다.
전체 코드
다음은 버튼과 연결된 핀의 Rising edge로 인터럽트 발생한 경우 LED를 토글하고 디바운싱을 통해 반복 처리를 방지하는 PCI 디바이스 드라이버 코드이다.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/pci.h>
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/jiffies.h>
#undef pr_fmt
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#define CH382L_VENDOR_ID 0x1C00
#define CH382L_DEVICE_ID 0x3050
#define BAR2 2
#define PDR_OFFSET 0x0
#define PSR_OFFSET 0x1
#define INTFLAG_BIT 1 << 6
#define D5OUT_BIT 1 << 5
#define DEBUOUNCE_INTERVAL msecs_to_jiffies(300)
struct ch382l_data {
void __iomem *bar2_base;
u8 pdr_value; // 현재 PDR 값
unsigned long last_debounce_time; // 직전 토글 타임
};
// 인터럽트 핸들러
static irqreturn_t ch382l_irq_handler(int irq, void *dev_id) {
struct ch382l_data *data = (struct ch382l_data *)dev_id;
u8 psr, new_pdr;
unsigned long now = jiffies;
// 디바운싱
if(time_before(now, data->last_debounce_time + DEBUOUNCE_INTERVAL)) {
return IRQ_HANDLED;
}
psr = ioread8(data->bar2_base + PSR_OFFSET);
// INTFLAG in PSR 확인
if(psr & INTFLAG_BIT) {
// INTFLAG가 켜져있으면 D5OUT 비트를 토글
new_pdr = data->pdr_value ^ D5OUT_BIT;
iowrite8(new_pdr, data->bar2_base + PDR_OFFSET);
data->pdr_value = new_pdr;
// 디바운싱 타임 기록
data->last_debounce_time = jiffies;
pr_info("Interrupt handled, D5OUT in PDR: %s\n", new_pdr & D5OUT_BIT ? "ON" : "OFF");
return IRQ_HANDLED; // IRQ 처리 완료
}
pr_debug("INTFLAG not set, IRQ ignored\n");
return IRQ_NONE; // IRQ 미처리
}
static int ch382l_probe(struct pci_dev *pdev, const struct pci_device_id *id) {
struct ch382l_data *data;
int ret;
pr_info("Probing device\n");
ret = pcim_enable_device(pdev);
if (ret) {
pr_err("Failed to enable PCI device\n");
return ret;
}
ret = pcim_iomap_regions(pdev, BIT(BAR2), "ch382l_bar");
if (ret) {
pr_err("Failed to map BAR2 I/O region\n");
return ret;
}
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if(!data) {
pr_err("Failed to allocate memory for device data\n");
return -ENOMEM;
}
pci_set_drvdata(pdev, data);
data->bar2_base = pcim_iomap_table(pdev)[BAR2];
if(!data->bar2_base) {
pr_err("Failed to read mapped BAR2\n");
return -ENOMEM;
}
// PDR 값 초기화
data->pdr_value = ioread8(data->bar2_base + PDR_OFFSET);
// 디바이스의 인터럽트 지원 여부 확인
// pdev->irq가 0이면 디바이스가 인터럽트를 사용하지 않는 것이고, 음수이면 IRQ 할당에 실패한 것
if(pdev->irq <= 0) {
pr_err("Invalid IRQ\n");
return -EINVAL;
}
// IRQ 요청
ret = request_irq(pdev->irq, ch382l_irq_handler, IRQF_SHARED, "ch382l_irq", data);
if(ret) {
pr_err("Failed to request IRQ\n");
return -ret;
}
pr_info("Device initialized successfully\n");
return 0;
}
// PCI 제거 함수
static void ch382l_remove(struct pci_dev *pdev) {
struct ch382l_data *data = pci_get_drvdata(pdev);
// IRQ 해제
free_irq(pdev->irq, data);
pr_info("Device removed\n");
}
// 지원하는 PCI 디바이스 ID 리스트
static const struct pci_device_id ch382l_pci_ids[] = {
{ PCI_DEVICE(CH382L_VENDOR_ID, CH382L_DEVICE_ID) },
{ 0, }
};
MODULE_DEVICE_TABLE(pci, ch382l_pci_ids);
static struct pci_driver ch382l_driver = {
.name = "ch382l_driver",
.id_table = ch382l_pci_ids,
.probe = ch382l_probe,
.remove = ch382l_remove,
};
module_pci_driver(ch382l_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("JSH");
MODULE_DESCRIPTION("A simple LKM using interrupt for a PCI to parallel port adapter CH382L");
결과
$ dmesg -w
'임베디드 개발 > 리눅스 디바이스 드라이버' 카테고리의 다른 글
LDD ] 커널 시간 관련 함수 사용법 (0) | 2025.02.27 |
---|---|
Linux Kernel ] Container_of (0) | 2025.02.06 |
Linux Kernel ] Error Handling (0) | 2025.02.03 |
Device tree compiler 사용법 (0) | 2025.01.27 |
LDD ] PCIe 디바이스 드라이버 작성하기 - (2) (0) | 2025.01.15 |