Linux Kernel(4.14) Hacks

rousalome.egloos.com

포토로그 Kernel Crash




[라즈베리파이] 인터럽트(3) - 인터럽트 핸들러 등록 #ES [라즈베리파이][커널]인터럽트

인터럽트 핸들러 등록 시 기본 파라미터
해당 인터럽트가 발생하면 해당 인터럽트 핸들러가 호출돼야 합니다. 이를 위해 디바이스 드라이버 코드에서 request_irq 함수을 써서 인터럽트 핸들러를 등록해야 합니다. request_irq 함수를 실제 어떻게 사용하는지 라즈베리 리눅스 커널 코드를 잠깐 살펴보겠습니다. 

bcm2708_fb_probe 함수에서 bcm2708_fb_dma_irq 함수를 인터럽트 핸들러로 등록합니다.
- 인터럽트 번호: fb->dma_irq
- 인터럽트 핸들러: bcm2708_fb_dma_irq
- 인터럽트 이름: "bcm2708_fb dma"
- dev_info: struct bcm2708_fb 타입의 디바이스 처리 인스턴스
- flags: 0x0(IRQF_TRIGGER_NONE)

request_irq 함수 마지막 파라미터는 fb란 포인터 변수인데, 이 주소를 dev_id로 등록합니다.
[drivers/video/fbdev/bcm2708_fb.c]
static int bcm2708_fb_probe(struct platform_device *dev)
{
struct device_node *fw_np;
struct rpi_firmware *fw;
struct bcm2708_fb *fb;
int ret;

fw_np = of_parse_phandle(dev->dev.of_node, "firmware", 0);
// ... skip ...
fb->dma_chan = ret;

ret = request_irq(fb->dma_irq, bcm2708_fb_dma_irq,
  0, "bcm2708_fb dma", fb);

디바이스 드라이버에서 request_irq 함수를 호출하지만 해당 코드를 열어보면 실제로 request_threaded_irq로 매핑함을 알 수 있습니다. request_threaded_irq 함수를 살펴보면 인터럽트 디스크립터 구조체 struct irq_desc로 메모리를 할당하고 디바이스 드라이버에서 채워준 파라미터를 아래 코드에서 설정합니다.
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
    const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

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)
{
struct irqaction *action;
struct irq_desc *desc;
int retval;
// ..생략..
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;

해당 인터럽트가 떠서 bcm2708_fb_dma_irq 인터럽트 핸들러가 호출되면, void 타입 dev_id 포인터는 struct bcm2708_fb 구조체로 캐스팅됩니다.


그럼 이제까지 배운 내용이 실제 어떻게 동작하는지 알아 보겠습니다. 코드를 짜서 실제 보드에서 어떤 로그가 찍히는지 확인해야 공부한 내용이 더 오래 머리 속에 남거든요.
 
다음 패치는 인터럽트 핸들러를 등록한 후 바로 해당 인터럽트 번호로 인터럽트 디스크립터를 커널 함수로 읽어와 인터럽트 설정 정보를 출력합니다. 

아래 패치는 인터럽트 핸들러를 등록한 후 바로 해당 인터럽트 정보를 커널에 재요청해서 커널 로그를 출력합니다. 제대로 인터럽트 핸들러를 등록했는지 점검하고 싶을 때 활용하면 좋은 코드입니다. 파라미터가 인터럽트 번호이므로 인터럽트 번호만 대입하면 다른 함수에서도 호출할 수 있습니다.
diff --git a/drivers/video/fbdev/bcm2708_fb.c b/drivers/video/fbdev/bcm2708_fb.c
index 612293c..afc91ea 100644
--- a/drivers/video/fbdev/bcm2708_fb.c
+++ b/drivers/video/fbdev/bcm2708_fb.c
@@ -804,6 +804,46 @@ static int bcm2708_fb_register(struct bcm2708_fb *fb)
    return ret;
 }
 
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/kernel_stat.h>
+
+static void interrupt_debug_irq_desc(int irq_num)
+{
+   struct irqaction *action;
+   struct irq_desc *desc;
+
+   int cpu_num = 0;
+
+   desc = irq_to_desc(irq_num);
+   
+   if (!desc ) {
+       pr_err("invalid desc at %s line: %d \n", __func__, __LINE__);
+       return;
+   }
+   
+   action = desc->action;
+
+   if (!action ) {
+       pr_err("invalid action at %s line: %d \n", __func__, __LINE__);
+       return;
+   }
+
+   printk("[+] irq_desc debug \n");
+
+   printk("irq num: %d name: %8s \n", action->irq , action->name);
+   printk("dev_id:0x%x \n", (unsigned int)action->dev_id);
+
+   if (action->handler)
+       printk("handler: %pF \n", action->handler);
+
+}
+
 static int bcm2708_fb_probe(struct platform_device *dev)
 {
    struct device_node *fw_np;
@@ -859,6 +899,7 @@ static int bcm2708_fb_probe(struct platform_device *dev)
        goto free_dma_chan;
    }

+   interrupt_debug_irq_desc(fb);

    pr_info("BCM2708FB: allocated DMA channel %d @ %p\n",
           fb->dma_chan, fb->dma_chan_base);

참고로 인터럽트 핸들러에서 schedule_work 함수를 호출해서 워크큐로 프로세스 레벨로 Bottom-Half 동작으로 처리하고 싶을 때가 있습니다. 이럴 때는 request_irq 함수로 인터럽트 핸들러를 등록하기 전 INIT_WORK로 struct work을 초기화할 필요가 있습니다. 인터럽트 핸들러를 등록하고 바로 인터럽트가 발생하면 해당 워크를 초기화하지 않아서 시스템 오동작을 할 수 있기 때문입니다.  

이제 인터럽트 핸들러를 살펴보겠습니다.
struct struct bcm2708_fb 구조체는 request_irq 함수 마지막 파라미터로 등록한 fb 변수 타입니다. &fb->dma_waitq에 등록된 waitqueue을 깨웁니다. 대부분 인터럽트 핸들러 코드와 마찬가지로 빨리 실행되도록 코드가 구성돼 있습니다.
[drivers/video/fbdev/bcm2708_fb.c]
static irqreturn_t bcm2708_fb_dma_irq(int irq, void *cxt)
{
struct bcm2708_fb *fb = cxt;

/* FIXME: should read status register to check if this is
* actually interrupting us or not, in case this interrupt
* ever becomes shared amongst several DMA channels
*
* readl(dma_chan_base + BCM2708_DMA_CS) & BCM2708_DMA_IRQ;
*/

/* acknowledge the interrupt */
writel(BCM2708_DMA_INT, fb->dma_chan_base + BCM2708_DMA_CS);

wake_up(&fb->dma_waitq);
return IRQ_HANDLED;
}

action->dev_id 타입으로 인터럽트 핸들을 쓰면 좋은 이유는 여러 디바이스를 인터럽트 핸들러에서 처리할 수 있기 때문입니다.

유용한 패치를 소개하겠습니다. 인터럽트 핸들러가 호출될 시점에 스택 덤프를 뜨면 해당 코드가 어떤 흐름으로 호출됐는지 알 수 있습니다. 아래 패치를 적용하면 해당 프로세스의 스택 메모리 덤프를 stack_dump 배열에 저장합니다.

인터럽트 처리가 마무리된 후 프로세스 레벨에서 stack_dump 배열에 저장된 메모리 덤프를 커널 로그로 출력하면, 인터럽트 벡터 심볼 __irq_svc과 스택에 푸쉬한 레지스터를 모두 볼 수 있습니다.
diff --git a/drivers/video/fbdev/bcm2708_fb.c b/drivers/video/fbdev/bcm2708_fb.c
index 612293c..2623e0d 100644
--- a/drivers/video/fbdev/bcm2708_fb.c
+++ b/drivers/video/fbdev/bcm2708_fb.c
@@ -713,9 +713,28 @@ static void bcm2708_fb_imageblit(struct fb_info *info,
        cfb_imageblit(info, image);
 }
 
+#define _DEBUG_ARM_STACK_SIZE 0x2000
+static unsigned int stack_dump[_DEBUG_ARM_STACK_SIZE];
+
+#define _DEBUG_ADDRESS_OFFSET 4
+
 static irqreturn_t bcm2708_fb_dma_irq(int irq, void *cxt)
 {
        struct bcm2708_fb *fb = cxt;
+       
+       register unsigned long current_sp asm ("sp");
+       unsigned long temp_stack_addr = 0x0;
+       unsigned int stack_bottom_addr = 0x0;
+       int proc_times = 0;
+       
+  stack_bottom_addr = (unsigned int)current->stack + _DEBUG_ARM_STACK_SIZE;// <<--[1]
+
+       temp_stack_addr = current_sp;  //<<--[2]
+       for (proc_times = 0; temp_stack_addr < stack_bottom_addr; proc_times++)
+       {
+        stack_dump[temp_stack_addr] = *((unsigned long*)(temp_stack_addr));  // <<--[3] 
+               temp_stack_addr += _DEBUG_ADDRESS_OFFSET; 
+       }
 
        /* FIXME: should read status register to check if this is
         * actually interrupting us or not, in case this interrupt

패치에 대해 간단히 설명을 드리면 아래와 같습니다.
[1]: 현재 구동 중인 프로세스의 태스크 디스크립터는 current란 매크로로 가져옵니다.
   struct task_struct->stack 멤버로 스택 Bottom 주소를 가져와서 프로세스 스택 사이즈를 더해 스택 Top 주소를 구합니다
[2]: "register unsigned long current_sp asm ("sp");" 명령어로 현재 실행 중인 코드의 스택 주소를 current_sp 변수가 가져옵니다. 이 값을 temp_stack_addr 로컬 변수에 저장합니다.
[3]: temp_stack_addr 현재 스택 주소에서 4바이트씩 계속 더하면서 스택 Top 주소까지 스택 메모리 덤프를 stack_dump 배열에 저장합니다.

인터럽트 핸들러 등록 시 플래그 설정
이 내용을 다루기 전에 인터럽트 신호에 대해서 조금 살펴보겠습니다.
가끔 인터럽트가 제대로 안 올라온다거나 디바이스 드라이버를 처음 올려서 기본 동작을 확인할 때 오실로스코프 장비로 인터럽트 파형을 측정할 때가 있습니다.

파형은 크게 인터럽트를 인지하는 방식에 따라 크게 에지-트리거 인터럽트와 레벨-트리거 인터럽트로 분류할 수 있습니다.

에지-트리거 인터럽트는 전기 신호가 어떤 상태에서 다른 상태로(보통은 하이(high) -> 로우(low)로) 넘어가는 순간 인터럽트를 감지하는 방식입니다. 반대로 레벨-트리거 인터럽트는 인터럽트 라인을 하이(high)로 유지할 때 인터럽트로 식별합니다.

커널에서 이미 인터럽트 신호를 어떻게 식별하는지에 따라 인터럽트를 설정하도록 이미 플래그를 정의해놨습니다.
[include/linux/interrupt.h]
#define IRQF_TRIGGER_NONE 0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010.

이렇게 인터럽트 신호 종류에 따라 인터럽트 핸들러를 등록할 때 적절한 파마리터를 설정해야 합니다. 이 파라미터는 request_irq 함수의 다섯 번째 플레그(unsigned long flags)로 채워 넣어야 합니다.
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
    const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

라즈베리파이 커널 코드를 보면 아래와 같이 mmc 드라이버에서 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING로 인터럽트 핸들러를 설정하는 코드를 볼 수 있습니다. 
[linux/drivers/mmc/host/mmc_spi.c]
void mmc_gpiod_request_cd_irq(struct mmc_host *host)
{
struct mmc_gpio *ctx = host->slot.handler_priv;
int ret, irq;
// ..생략..

if (irq >= 0) {
if (!ctx->cd_gpio_isr)
ctx->cd_gpio_isr = mmc_gpio_cd_irqt;
ret = devm_request_threaded_irq(host->parent, irq,
NULL, ctx->cd_gpio_isr,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
ctx->cd_label, host);

참고로 IRQF_ONESHOT 플래그는 인터럽트 핸들러에서 인터럽트를 처리하는 동안 인터럽트를 다시 처리하지 않게 설정합니다. 그리고 request_threaded_irq 대신 왜 devm_request_threaded_irq 함수을 쓰는 이유는 인터럽트를 disable_irq를 써서 인터럽트 등록 해지할 때 메모리에서 해당 인터럽트 디스크립터를 해제할 수 있기 때문입니다.




덧글

댓글 입력 영역