Interrupts -11- (IC Driver for rpi2)

<kernel v4.0>

Interrupts -11- (IC Driver for rpi2)

Device Tree로 hierarchy irq chip 구성

설명에 사용된 샘플 보드는 다음과 같다.

  • 64비트 rock960
    • GIC v3 인터럽트 컨트롤러
    • rock 사의 rk3399칩셋은 GIC v3를 사용한다.
    • 커널은 v5.4를 기준으로 설명하였다.
  • 32비트 rpi2
    • BCM2836 전용 인터럽트 컨트롤러
    • 커널은 v4.4를 기준으로 설명하였다.

 

다음 그림은 rpi2에서 설정되는 5개의 irq_chip을 보여준다.

3 개의 인터럽트 컨트롤러

  • gpio pinctrl 디바이스
    • gpio를 사용하는 pinctrl 디바이스 드라이버에 구현되어 있다.
      • 인터럽트 컨트롤러 디바이스 드라이버가 아니므로 IRQCHIP_DECLARE()를 사용하지 않았으므로 __chip_id_of_table 섹션에 인터럽트 컨트롤러 엔트리가 등록되지 않는다.
        • 인터럽트 초기화 루틴 irqchip_init()에서 초기화 함수를 호출하지 않는다.
    • DT 스크립트에 “gpio-controller;” 및 “interrupt-controller;” 두 가지 기능의 컨트롤러가 동작한다.
      • 메인 기능이 gpio를 사용하는 pinctrl 디바이스이다.
      • 추가 기능으로 gpio에서 인터럽트 라인 컨트롤 기능을 수행할 수 있다.
        • gpio pin 들을 인터럽트 라인으로 사용할 수 있어서 이들에 대한 set/clear 등의 마스킹 동작을 위해 irq_chip이 구현되어 있다.
  • GPU 인터럽트 컨트롤러
    • 0 ~ 2번 뱅크를 입력으로 받아들이며 각각 최대 32개의 인터럽트 라인을 가지고 있다.
    •  rpi에서도 사용하는 인터럽트 컨트롤러
  • LOCAL 인터럽트 컨트롤러
    • rpi2에서 SMP core 간 IPI 등의 소프트 인터럽트 처리 등을 위해 별도로 추가되었다.
    • 3 개의 irq_chip으로 구성
      • gpu 인터럽트를 처리하기 위한 irq_chip
      • 4개의 timer 인터럽트를 처리하기 위한 irq_chip
      • 1개의 pmu 인터럽트를 처리하기 위한 irq_chip

 


DT용 인터럽트 컨트롤러 초기화 – rpi2

 

부모 컨트롤러 Part

bcm2836_arm_irqchip_l1_intc_of_init()

drivers/irqchip/irq-bcm2836.c

static int __init bcm2836_arm_irqchip_l1_intc_of_init(struct device_node *node,
                                                      struct device_node *parent)
{
        intc.base = of_iomap(node, 0);
        if (!intc.base) {
                panic("%s: unable to map local interrupt registers\n",
                        node->full_name);
        }

        bcm2835_init_local_timer_frequency();

        intc.domain = irq_domain_add_linear(node, LAST_IRQ + 1,
                                            &bcm2836_arm_irqchip_intc_ops,
                                            NULL);
        if (!intc.domain)
                panic("%s: unable to create IRQ domain\n", node->full_name);

        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_CNTPSIRQ,
                                         &bcm2836_arm_irqchip_timer);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_CNTPNSIRQ,
                                         &bcm2836_arm_irqchip_timer);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_CNTHPIRQ,
                                         &bcm2836_arm_irqchip_timer);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_CNTVIRQ,
                                         &bcm2836_arm_irqchip_timer);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_GPU_FAST,
                                         &bcm2836_arm_irqchip_gpu);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_PMU_FAST,
                                         &bcm2836_arm_irqchip_pmu);

        bcm2836_arm_irqchip_smp_init();

        set_handle_irq(bcm2836_arm_irqchip_handle_irq);
        return 0;
}

bcm2836 로컬 인터럽트에 해당하는 1개의  irq domain 과 3 개의 irq chip을 구성하고 사용하는 인터럽트들을 매핑한다.

  • 코드 라인 4~8에서 인수로 전달받은 인터럽트 컨트롤러 노드의 base 레지스터를 IO 매핑하여 가상주소를 알아온다. 만일 매핑이 실패하면 panic 처리를 한다.
    • rpi2: 0xf400_0000
  • 코드 라인 10~14에서 10개의 irq로 리니어 domain을 추가한다. domain의 변환 함수가 ops에 등록되어 운용된다. 만일 irq domain이 추가되지 않는 경우 panic 처리를 한다.
    • .xlate = irq_domain_xlate_onecell
  • 코드 라인 18~25에서 4개의 타이머 인터럽트를 timer용 irq chip에 구성하고 매핑한다.
  • 코드 라인 26~27에서 1개의 gpu 인터럽트를 gpu용 irq chip에 구성하고 매핑한다.
  • 코드 라인 28~29에서 1개의 pmu 인터럽트를 pmu용 irq chip에 구성하고 매핑한다.
  • 코드 라인 31에서 cpu on/off 등 상태 변경에 따른 notify를 받기 위해 연동하고, ipi 호출을 위해 연동함수를 등록한다.
  • 코드 라인 33에서 irq exception 처리 루틴에서 호출하도록 irq 핸들러 함수를 전역 handle_arch_irq에 등록한다.

 

자식 컨트롤러 Part

bcm2836_armctrl_of_init()

drivers/irqchip/irq-bcm2835.c

static int __init bcm2836_armctrl_of_init(struct device_node *node,
                                          struct device_node *parent)
{
        return armctrl_of_init(node, parent, true);
}

처리할 인터럽트 컨트롤러에 대한 디바이스 노드와 상위 인터럽트 컨트롤러에 대한 디바이스 노드 정보를 갖고 armctrl_of_init() 함수를 호출한다.

 

armctrl_of_init()

drivers/irqchip/irq-bcm2835.c

static int __init armctrl_of_init(struct device_node *node,
                                  struct device_node *parent,
                                  bool is_2836)
{
        void __iomem *base;
        int irq, b, i;

        base = of_iomap(node, 0);
        if (!base)
                panic("%s: unable to map IC registers\n",
                        node->full_name);

        intc.base = base;
        intc.domain = irq_domain_add_linear(node, NUMBER_IRQS * 2,
                                            &armctrl_ops, NULL);
        if (!intc.domain)
                panic("%s: unable to create IRQ domain\n", node->full_name);

        for (b = 0; b < NR_BANKS; b++) { 
                intc.pending[b] = base + reg_pending[b];
                intc.enable[b] = base + reg_enable[b];
                intc.disable[b] = base + reg_disable[b];

                for (i = 0; i < bank_irqs[b]; i++) {
                        irq = irq_create_mapping(intc.domain, MAKE_HWIRQ(b, i));
                        BUG_ON(irq <= 0);
                        irq_set_chip_and_handler(irq, &armctrl_chip,
                                handle_level_irq);
                        irq_set_probe(irq);
                }
        }

        if (is_2836) {
                int parent_irq = irq_of_parse_and_map(node, 0);

                if (!parent_irq) {
                        panic("%s: unable to get parent interrupt.\n",
                              node->full_name);
                }
                irq_set_chained_handler(parent_irq, bcm2836_chained_handle_irq);
        } else {
                set_handle_irq(bcm2835_handle_irq);
        }

        if (is_2836) {
                intc.local_regmap =
                        syscon_regmap_lookup_by_compatible("brcm,bcm2836-arm-local");
                if (IS_ERR(intc.local_regmap)) {
                        pr_err("Failed to get local register map. FIQ is disabled for cpus > 1\n");
                        intc.local_regmap = NULL;
                }
        }

        /* Make a duplicate irq range which is used to enable FIQ */
        for (b = 0; b < NR_BANKS; b++) {
                for (i = 0; i < bank_irqs[b]; i++) {
                        irq = irq_create_mapping(intc.domain,
                                        MAKE_HWIRQ(b, i) + NUMBER_IRQS);
                        BUG_ON(irq <= 0); 
                        irq_set_chip(irq, &armctrl_chip);
                        irq_set_probe(irq);
                }
        }
        init_FIQ(FIQ_START);

        return 0;
}

 

bcm2836 gpu 인터럽트에 해당하는 irq domain 과 irq chip을 구성하고 사용하는 인터럽트들을 매핑한다.

  • 코드 라인 8~13에서 인수로 전달받은 인터럽트 컨트롤러 노드의 base 레지스터를 IO 매핑하여 가상주소를 알아온다. 만일 매핑이 실패하면 panic 처리를 한다.
    • rpi2: 0xf300_b200
  • 코드 라인 14~17에서 3개 뱅크(각 32개 irq)가 연결되는 96개 irq + 96개 fiq에 대응하도록 irq domain을 구성하고 매핑한다.
    • irq domain의 ops
      • .xlate =armctrl_xlate
  • 코드 라인 19~22에서 각 뱅크의 pending 레지스터와 enable 및 disable을 레지스터의 가상 주소를 대입한다.
  • 코드 라인 24~25에서 각 뱅크에서 처리할 hw 인터럽트 수 만큼을 irq domain과 irq 디스크립터에 매핑한다.
    • 3 개의 각 뱅크에서 매핑할 hwirq 들
      • bank#0
        • hwirq=0~7까지 8개
        • irq=22~29
      • bank#1
        • hwirq=32~63까지 32개
        • irq=32~63
      • bank#2
        • hwirq=64~95까지 32개
        • irq=62~93
  • 코드 라인 27~30에서 “ARMCTRL-level” 이라는 이름의 irq chip을 설정하고 핸들러로 handle_level_irq()를 사용한다.
  • 코드 라인 33~40에서 이 드라이버 코드는 bcm2835(rpi)와 혼용하여 사용하므로 bcm2836(rpi2)인 경우 부모 인터럽트 컨트롤의 hw irq를 알아와서 이 인터럽트 핸들러로 bcm2836_chained_handle_irq() 함수를 대입한다. 만일 Device Tree 스크립트에서 parent irq 번호를 알아오지 못한 경우 panic 처리한다.
    • 모든 gpu 인터럽트들 중 하나라도 인터럽트가 발생하면 부모 인터럽트 컨트롤러의 8번 hwirq도 인터럽트가 발생한다.
  • 코드 라인 41~43에서 bcm2835(rpi)에서 호출한 경우 핸들러로 bcm2835_handle_irq() 함수를 대입한다.
  • 코드 라인 45~52에서 Device Tree에서 System Control Register 정보를 읽어서 base 주소를 대입한다. 못 읽어오는 경우 메시지를 출력한다. 이 때 2 번째 cpu 부터 fiq 인터럽트를 수신할 수 없게된다.
    • rpi2: 물리 주소=0x4000_0000, size=0x100
  • 코드 라인 55~58에서 각 뱅크에서 처리할 hw 인터럽트 수 만큼을 irq domain과 irq 디스크립터에 매핑한다. 이 인터럽트들은 fiq에 해당한다.
    • 3 개의 각 뱅크에서 매핑할 hwirq 들
      • bank#0
        • hwirq=96~103까지 8개
        • irq=94~101
      • bank#1
        • hwirq=128~159까지 32개
        • irq=102~133
      • bank#2
        • hwirq=160~191까지 32개
        • irq=134~165
  • 코드 라인 60~62에서 “ARMCTRL-level” 이라는 이름의 irq chip을 설정하고 probe를 설정한다.
  • 코드 라인 64에서 default FIQ 인터럽트 처리기를 백업하기 위해 fiq 벡터 값과 fiq 모드에서 사용하는 레지스터들 일부(r8 ~ r12, sp, lr)를 백업해둔다.
    • fiq 사용 시 멀티플 레지스터 로드 및 저장 시 깨질 수 있기에 복원을 위해 초기 핸들러를 백업해 둔다.

 

 

GPIO 컨트롤러 Part

인터럽트 컨트롤러에서 초기화하지 않고 GPIO의 pinctrl 드라이버에서 초기화된다.  초기화 과정의 소스는 여기서 설명하지 않고 아래 그림만 참고한다.

 


IRQ exception 처리 후 컨트롤러가 설정한 핸들러 함수 호출

부모 컨트롤러  Part

bcm2836_arm_irqchip_handle_irq()

static void
__exception_irq_entry bcm2836_arm_irqchip_handle_irq(struct pt_regs *regs)
{
        int cpu = smp_processor_id();
        u32 stat;

        stat = readl_relaxed(intc.base + LOCAL_IRQ_PENDING0 + 4 * cpu);
        if (stat & BIT(LOCAL_IRQ_MAILBOX0)) {
#ifdef CONFIG_SMP
                void __iomem *mailbox0 = (intc.base +
                                          LOCAL_MAILBOX0_CLR0 + 16 * cpu);
                u32 mbox_val = readl(mailbox0);
                u32 ipi = ffs(mbox_val) - 1;

                writel(1 << ipi, mailbox0);
                handle_IPI(ipi, regs);
#endif
        } else if (stat) {
                u32 hwirq = ffs(stat) - 1;

                handle_IRQ(irq_linear_revmap(intc.domain, hwirq), regs);
        }
}

mailbox0 레지스터를 읽어 pending irq가 있는 경우 IPI 핸들러를 호출하고 그렇지 않은 경우 IRQ 핸들러를 호출한다.

  • rpi2: 부모 인터럽트 컨틀롤러에서 발생한 irq들은 4개의 timer와 pmu 인터럽트이다.

 

  • 코드 라인 7~8에서 현재 cpu에 대한 local pending 레지스터를 읽어서 bit4를 통해 mailbox가 수신되었는지 확인한다. 만일 수신된 경우
  • 코드 라인 9~17에서 수신된 mailbox에서 가장 먼저(msb) 처리할 IPI 번호를 읽어 그 비트를 클리어하고 handle_IPI() 함수를 호출하여 IPI 처리를 수행하게 한다.
  • 코드 라인 18~22에서 hwirq  -> irq 번호로 reversemap을 사용하여 transalation하여 얻은 번호로 handle_irq()를 호출한다.

 

자식 컨트롤러  Part

bcm2836_chained_handle_irq()

drivers/irqchip/irq-bcm2835.c

 

static void bcm2836_chained_handle_irq(struct irq_desc *desc)
{
        u32 hwirq;

        while ((hwirq = get_next_armctrl_hwirq()) != ~0)
                generic_handle_irq(irq_linear_revmap(intc.domain, hwirq));
}

gpu pending 레지스터 값을 읽어서 hwirq 값을 irq로 변환한 후 generic_handle_irq() 함수를 호출한다.

  • 변환한 irq에 등록한 핸들러 함수를 호출한다.
    • rpi2: handle_level_irq()

 

get_next_armctrl_hwirq()

drivers/irqchip/irq-bcm2835.c

static u32 get_next_armctrl_hwirq(void)
{
        u32 stat = readl_relaxed(intc.pending[0]) & BANK0_VALID_MASK;

        if (stat == 0)
                return ~0;
        else if (stat & BANK0_HWIRQ_MASK)
                return MAKE_HWIRQ(0, ffs(stat & BANK0_HWIRQ_MASK) - 1);
        else if (stat & SHORTCUT1_MASK)
                return armctrl_translate_shortcut(1, stat & SHORTCUT1_MASK);
        else if (stat & SHORTCUT2_MASK)
                return armctrl_translate_shortcut(2, stat & SHORTCUT2_MASK);
        else if (stat & BANK1_HWIRQ)
                return armctrl_translate_bank(1);
        else if (stat & BANK2_HWIRQ)
                return armctrl_translate_bank(2);
        else
                BUG();
}

gpu pending 레지스터 값을 읽어서 hwirq로 변환한다.

  • 코드 라인 3에서 로컬 pending  레지스터 값을 읽어서 관련 비트들만 마스크한다.
    • timer 4개, mailbox 4개, gpu 매핑 1개, pmu 매핑 1개, shortcut 11개 비트들
  • 코드 라인 5~6에서 pending 인터럽트가 없는 경우 ~0(호출 함수에서 루프의 끝을 의미)을 반환한다.
  • 코드 라인 7~8에서 local 인터럽트 8개(bit7:0) 중 하나에 속한 경우 0번 뱅크에 해당하는 hwirq로 변환하여 반환한다.
  • 코드 라인 9~10에서 shortcut1에 해당하는 인터럽트가 발생한 경우 1번 뱅크에 해당하는 hwirq로 변환하여 반환한다.
    • bit10 ~ bit14 -> <7, 9, 10, 18, 19> + 32(bank1)
  • 코드 라인 11~12에서 shortcut2에 해당하는 인터럽트가 발생한 경우 2번 뱅크에 해당하는 hwirq로 변환하여 반환한다.
    • bit15 ~ bit20 -> <21, 22, 23, 24, 25, 30> + 64(bank2)
  • 코드 라인 13~14에서 bank1에 해당하는 인터럽트인 경우 gpu#1 pending 레지스터를 읽어 가장 우측 비트(lsb)가 1로 설정된 인터럽트에 해당하는 hwirq로 변환하여 반환한다.
    • bit0 ~ bit31 -> 30 ~ 61
  • 코드 라인 15~16에서 bank2에 해당하는 인터럽트인 경우 gpu#2 pending 레지스터를 읽어 가장 우측 비트(lsb)가 1로 설정된 인터럽트에 해당하는 hwirq로 변환하여 반환한다.
    • bit0 ~ bit31 -> 62 ~ 93

 

armctrl_translate_shortcut()

drivers/irqchip/irq-bcm2835.c

static u32 armctrl_translate_shortcut(int bank, u32 stat)
{
        return MAKE_HWIRQ(bank, shortcuts[ffs(stat >> SHORTCUT_SHIFT) - 1]);
}

shortcut에 해당하는 인터럽트가 발생한 경우 해당 뱅크에 해당하는 hwirq로 변환하여 반환한다.

  • bit10 ~ bit14 -> <7, 9, 10, 18, 19> + 32(bank1)
  • bit15 ~ bit20 -> <21, 22, 23, 24, 25, 30> + 64(bank2)

 

armctrl_translate_bank()

drivers/irqchip/irq-bcm2835.c

/*
 * Handle each interrupt across the entire interrupt controller.  This reads the
 * status register before handling each interrupt, which is necessary given that
 * handle_IRQ may briefly re-enable interrupts for soft IRQ handling.
 */

static u32 armctrl_translate_bank(int bank)
{
        u32 stat = readl_relaxed(intc.pending[bank]);

        return MAKE_HWIRQ(bank, ffs(stat) - 1);
}

gpu#1 또는 gpu#2 pending 레지스터에서 읽은 stat 값에서 가장 우측 비트에 해당하는 hwirq를 반환한다.

  • bit0 ~ bit31 -> 30 ~ 61
  • bit0 ~ bit31 -> 62 ~ 93

 

drivers/irqchip/irq-bcm2835.c

/* Put the bank and irq (32 bits) into the hwirq */
#define MAKE_HWIRQ(b, n)        ((b << 5) | (n))
#define HWIRQ_BANK(i)           (i >> 5)
#define HWIRQ_BIT(i)            BIT(i & 0x1f)

 


irq chip의 콜백 함수 – rpi2

ARMCTRL-level용

armctrl_chip

drivers/irqchip/irq-bcm2835.c

static struct irq_chip armctrl_chip = {
        .name = "ARMCTRL-level",
        .irq_mask = armctrl_mask_irq,
        .irq_unmask = armctrl_unmask_irq
};

bcm2835 인터럽트용 irq_chip 콜백 함수들

 

armctrl_mask_irq()

drivers/irqchip/irq-bcm2835.c

static void armctrl_mask_irq(struct irq_data *d)
{
        if (d->hwirq >= NUMBER_IRQS)
                writel_relaxed(REG_FIQ_DISABLE, intc.base + REG_FIQ_CONTROL);
        else
                writel_relaxed(HWIRQ_BIT(d->hwirq),
                               intc.disable[HWIRQ_BANK(d->hwirq)]);
}

요청한 인터럽트(hwirq) 라인을 마스크하여 인터럽트 진입을 허용하지 않게 한다.

  • 코드 라인 3~4에서 hwirq가 NUMBER_IRQS(96) 이상인 경우 FIQ 전체에 대한 인터럽트 마스크를 수행한다.
  • 코드 라인 5~7에서 요청 IRQ에 대한 마스크를 수행한다.
    • 1개 뱅크당 32개의 인터럽트 라인을 제어할 수 있다.

 

armctrl_unmask_irq()

drivers/irqchip/irq-bcm2835.c

static void armctrl_unmask_irq(struct irq_data *d)
{
        if (d->hwirq >= NUMBER_IRQS) {
                if (num_online_cpus() > 1) {
                        unsigned int data;
                        int ret;

                        if (!intc.local_regmap) {
                                pr_err("FIQ is disabled due to missing regmap\n");
                                return;
                        }

                        ret = regmap_read(intc.local_regmap,
                                          ARM_LOCAL_GPU_INT_ROUTING, &data);
                        if (ret) {
                                pr_err("Failed to read int routing %d\n", ret);
                                return;
                        }

                        data &= ~0xc;
                        data |= (1 << 2);
                        regmap_write(intc.local_regmap,
                                     ARM_LOCAL_GPU_INT_ROUTING, data);
                }

                writel_relaxed(REG_FIQ_ENABLE | hwirq_to_fiq(d->hwirq),
                               intc.base + REG_FIQ_CONTROL);
        } else {
                writel_relaxed(HWIRQ_BIT(d->hwirq),
                               intc.enable[HWIRQ_BANK(d->hwirq)]);
        }
}

요청한 인터럽트(hwirq) 라인을 언마스크하여 인터럽트 진입을 허용하게 한다.

  • 코드 라인 3에서 hwirq가 NUMBER_IRQS(96) 이상인 경우
  • 코드 라인 4~24에서 online cpu가 1개를 초과하는 경우 2개 이상인 경우 bcm2836에 있는 local routing 레지스터를 설정할 수 있고, 여기에서 irq cpu 라우팅을 fiq cpu 라우팅으로 변경한다.
    • local routing  레지스터 값은 하위 3비트만 유효하다.
      • 최하위 비트 2개는 cpu core 번호
      • bit2는 0=irq, 1=fiq
    • 이 조작으로 영향을 받는 인터럽트는 core 전용이 아닌 공통 인터럽트들로 아래와 같다.
      • GPU IRQ
      • Local Timer
      • AXI error
      • 미 사용 15개의 local peripheral 인터럽트들
    • 이 조작으로 영향을 받지 않는 인터럽트들은 core 인터럽트로 아래와 같다.
      • 4개의 코어 타이머 인터럽트들 x 4 cpus
      • 1개의 pm 인터럽트 x 4 cpus
      • 4개의 mailbox 인터럽트들 x 4 cpus
  • 코드 라인 26~27에서 FIQ Control 레지스터의 8번 비트를 켜고 bit0~bit7의 irq(fiq) source를 지정한다.
    • FIQ Control 레지스터: 버스(VC) 주소=0x7e00_b20c, ARM 물리 주소=0x3f00_b20c, ARM 가상 주소=0xf300_b20c
    • 1개의 irq 소스를 fiq로 라우팅할 수 있다.
      • rpi2: 고석 처리가 요구되는 usb를 fiq로 라우팅하여 사용한다.
  • 코드 라인 28~31에서 요청 IRQ를 enable을 시킨다.
    • 3개의 뱅크에서 각 뱅크당 최대 32개의 인터럽트 라인을 제어할 수 있다.

 


Argument 파싱 for DT

armctrl_xlate() – RPI2

arch/arm/mach-bcm2709/armctrl.c

/* from drivers/irqchip/irq-bcm2835.c */
static int armctrl_xlate(struct irq_domain *d, struct device_node *ctrlr,
        const u32 *intspec, unsigned int intsize,
        unsigned long *out_hwirq, unsigned int *out_type)
{
        if (WARN_ON(intsize != 2))
                return -EINVAL;

        if (WARN_ON(intspec[0] >= NR_BANKS))
                return -EINVAL;

        if (WARN_ON(intspec[1] >= IRQS_PER_BANK))
                return -EINVAL;

        if (WARN_ON(intspec[0] == 0 && intspec[1] >= NR_IRQS_BANK0))
                return -EINVAL;

        if (WARN_ON(intspec[0] == 3 && intspec[1] > 3 && intspec[1] != 5 && intspec[1] != 9))
                return -EINVAL;

        if (intspec[0] == 0) 
                *out_hwirq = ARM_IRQ0_BASE + intspec[1];
        else if (intspec[0] == 1)
                *out_hwirq = ARM_IRQ1_BASE + intspec[1];
        else if (intspec[0] == 2)
                *out_hwirq = ARM_IRQ2_BASE + intspec[1];
        else
                *out_hwirq = ARM_IRQ_LOCAL_BASE + intspec[1];

        /* reverse remap_irqs[] */
        switch (*out_hwirq) {
        case INTERRUPT_VC_JPEG:
                *out_hwirq = INTERRUPT_JPEG;
                break;
        case INTERRUPT_VC_USB:
                *out_hwirq = INTERRUPT_USB;
                break;
        case INTERRUPT_VC_3D:
                *out_hwirq = INTERRUPT_3D;
                break;
        case INTERRUPT_VC_DMA2:
                *out_hwirq = INTERRUPT_DMA2;
                break;
        case INTERRUPT_VC_DMA3:
                *out_hwirq = INTERRUPT_DMA3;
                break;
        case INTERRUPT_VC_I2C:
                *out_hwirq = INTERRUPT_I2C;
                break;
        case INTERRUPT_VC_SPI:
                *out_hwirq = INTERRUPT_SPI;
                break;
        case INTERRUPT_VC_I2SPCM:
                *out_hwirq = INTERRUPT_I2SPCM;
                break;
        case INTERRUPT_VC_SDIO:
                *out_hwirq = INTERRUPT_SDIO;
                break;
        case INTERRUPT_VC_UART:
                *out_hwirq = INTERRUPT_UART;
                break;
        case INTERRUPT_VC_ARASANSDIO:
                *out_hwirq = INTERRUPT_ARASANSDIO;
                break;
        }

        *out_type = IRQ_TYPE_NONE;
        return 0;
}

DT 스크립트에 설정한 argumnet 값으로 hwirq 번호를 알아온다.

  • 인터럽트를 사용하는 디바이스는 “interrupts = { bank, irq }” 속성 값을 전달한다.
    • bcm2708(rpi) 및 bcm2709(rpi2)에서는 2 개의 인수를 받는데 처음 것은 bank 이고, 두 번째 것은 각 뱅크에 해당하는 개별 인터럽트 번호(0~31번 )이다.
    • rpi2 예) out_hwirq = 0~85, 96~99, 101, 105

 

  • 코드 라인 6~7에서 DT 스크립트에서 설정한 “interrupts = { bank, irq }” 형태로 인수가 2개가 아닌 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 9~10에서 bank 값이 4 이상인 경우 -EINVAL 에러를 반환한다.
    • rpi: 0~2까지 가능
    • rpi2: 0~3까지 가능
  • 코드 라인 12~13에서 irq 값이 bank당 최대 값 수(32) 이상인 경우 -EINVAL 에러를 반환한다.
    • rpi & rpi2: bank당 0~31까지 가능
  • 코드 라인 15~16에서 0번 bank를 사용하는 irq 값이 bank #0 최대 인터럽트 수 이상인 경우 -EIVAL 에러를 반환한다.
    • rpi & rpi2: bank당 0~20까지 가능
  • 코드 라인 18~19에서 3번 bank의 경우 irq 번호가 0~3, 5 및 9번이 아닌 경우 -EINVAL 에러를 반환한다.
    • 0~3: core timer, 5=mail box #1, 9=pmu
  • 코드 라인 21~22에서 bank 0번의 경우 64번 부터 hw 인터럽트가 시작된다.
  • 코드 라인 23~24에서 bank 1번의 경우 0번 부터 hw 인터럽트가 시작된다.
  • 코드 라인 25~26에서 bank 2번의 경우 32번 부터 hw 인터럽트가 시작된다.
  • 코드 라인 27~28에서 bank 3번의 경우 96번 부터 hw 인터럽트가 시작된다.
  • 코드 라인 41~65에서 Video Core에서 사용하는 인터럽트들(#0 ~ #64) 중 11개가 공유되어 Local ARM에 라우팅(#74~#84) 된다.
    • 예) 7(vc_usb)  -> 74(arm_usb)
  • 코드 라인 67~68에서 에러가 없는 경우 out_type에 IRQ_TYPE_NONE을 대입한후 0을 반환한다.

 


GPIO pinctrl-bcm2835용

rpi 및 rpi2에는 2개의 뱅크에 나뉘어 총 54개의 gpio 라인이 제공되고 이를 28개, 18개, 8개로 나누어 81~83번의 hw 인터럽트를 발생시킨다.

 

bcm2835_gpio_irq_chip

drivers/pinctrl/bcm/pinctrl-bcm2835.c

static struct irq_chip bcm2835_gpio_irq_chip = {
        .name = MODULE_NAME,
        .irq_enable = bcm2835_gpio_irq_enable,
        .irq_disable = bcm2835_gpio_irq_disable,
        .irq_set_type = bcm2835_gpio_irq_set_type,
};

bcm2835의 gpio 인터럽트용 irq_chip 콜백 함수들

 

bcm2835_gpio_irq_enable()

drivers/pinctrl/bcm/pinctrl-bcm2835.c

static void bcm2835_gpio_irq_enable(struct irq_data *data)
{
        struct bcm2835_pinctrl *pc = irq_data_get_irq_chip_data(data);
        unsigned gpio = irqd_to_hwirq(data);
        unsigned offset = GPIO_REG_SHIFT(gpio);
        unsigned bank = GPIO_REG_OFFSET(gpio);
        unsigned long flags;

        spin_lock_irqsave(&pc->irq_lock[bank], flags);
        set_bit(offset, &pc->enabled_irq_map[bank]);
        bcm2835_gpio_irq_config(pc, gpio, true);
        spin_unlock_irqrestore(&pc->irq_lock[bank], flags);
}

gpio irq를 enable 한다. (hwirq=0~53)

  • 코드 라인 3에서 irq_chip->chip_data에 저장해둔 pc(pin control)를 알아온다.
  • 코드 라인 4에서 gpio hwirq 번호를 알아온다.
  • 코드 라인 5~6에서 hwirq에 대한 bank와 offset을 구한다.
    • 예) hwirq=53인 경우 bank=1, offset=21
  • 코드 라인 10에서 hwirq에 해당하는 enable 비트를 설정한다.
  • 코드 라인 11에서 hwirq의 트리거 타입에 따른 gpio 레지스터를 조작하여 enable 설정을 한다.

 

bcm2835_gpio_irq_disable()

drivers/pinctrl/bcm/pinctrl-bcm2835.c

static void bcm2835_gpio_irq_disable(struct irq_data *data)
{
        struct bcm2835_pinctrl *pc = irq_data_get_irq_chip_data(data);
        unsigned gpio = irqd_to_hwirq(data);
        unsigned offset = GPIO_REG_SHIFT(gpio);
        unsigned bank = GPIO_REG_OFFSET(gpio);
        unsigned long flags;

        spin_lock_irqsave(&pc->irq_lock[bank], flags);
        bcm2835_gpio_irq_config(pc, gpio, false);
        /* Clear events that were latched prior to clearing event sources */
        bcm2835_gpio_set_bit(pc, GPEDS0, gpio);
        clear_bit(offset, &pc->enabled_irq_map[bank]);
        spin_unlock_irqrestore(&pc->irq_lock[bank], flags);
}

gpio irq를 disable 한다. (hwirq=0~53)

  • 코드 라인 3에서 irq_chip->chip_data에 저장해둔 pc(pin control)를 알아온다.
  • 코드 라인 4에서 hwirq 번호를 알아온다.
    • rpi2: 81 ~ 83
  • 코드 라인 5~6에서 hwirq에 대한 bank와 offset을 구한다.
    • 예) hwirq=81인 경우 bank=2, offset=17
  • 코드 라인 10에서 hwirq의 트리거 타입에 따른 gpio 레지스터를 조작하여 disable 설정을 한다.
  • 코드 라인 12에서 2 개의 GPEDS(GPIO Event Detect Status) Register 중 요청 hwirq에 해당하는 비트를 1로 기록하여 이벤트 발생 비트를 클리어한다.
  • 코드 라인 13에서 hwirq에 해당하는 enable 비트를 클리어한다.

 

bcm2835_gpio_irq_set_type()

drivers/pinctrl/bcm/pinctrl-bcm2835.c

static int bcm2835_gpio_irq_set_type(struct irq_data *data, unsigned int type)
{
        struct bcm2835_pinctrl *pc = irq_data_get_irq_chip_data(data);
        unsigned gpio = irqd_to_hwirq(data);
        unsigned offset = GPIO_REG_SHIFT(gpio);
        unsigned bank = GPIO_REG_OFFSET(gpio);
        unsigned long flags;
        int ret;

        spin_lock_irqsave(&pc->irq_lock[bank], flags);

        if (test_bit(offset, &pc->enabled_irq_map[bank]))
                ret = __bcm2835_gpio_irq_set_type_enabled(pc, gpio, type);
        else 
                ret = __bcm2835_gpio_irq_set_type_disabled(pc, gpio, type);

        spin_unlock_irqrestore(&pc->irq_lock[bank], flags);

        return ret;
}

gpio irq에 해당하는 트리거 타입을 설정한다. (hwirq=0~53)

  • 타입 설정 전에 요청 irq에 대해 disable 한 후 트리거 타입을 바꾼다. 그런 후 enable 비트가 설정된 경우 해당 irq에 대해 enable 시킨다.

 


bcm2836-gpu용

bcm2836_arm_irqchip_gpu

drivers/irqchip/irq-bcm2836.c

static struct irq_chip bcm2836_arm_irqchip_gpu = {
        .name           = "bcm2836-gpu",
        .irq_mask       = bcm2836_arm_irqchip_mask_gpu_irq,
        .irq_unmask     = bcm2836_arm_irqchip_unmask_gpu_irq,
};

bcm2836의 GPU 인터럽트용 irq_chip 콜백 함수들

 

bcm2836_arm_irqchip_mask_gpu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_mask_gpu_irq(struct irq_data *d)
{
}

1개 gpu 인터럽트의 mask 처리 요청에 대해 아무것도 수행하지 않는다.

 

bcm2836_arm_irqchip_unmask_gpu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_unmask_gpu_irq(struct irq_data *d)
{
}

1개 gpu 인터럽트의 unmask 처리 요청에 대해 아무것도 수행하지 않는다.

 

bcm2836-pmu용

bcm2836_arm_irqchip_pmu

drivers/irqchip/irq-bcm2836.c

static struct irq_chip bcm2836_arm_irqchip_pmu = {
        .name           = "bcm2836-pmu",
        .irq_mask       = bcm2836_arm_irqchip_mask_pmu_irq,
        .irq_unmask     = bcm2836_arm_irqchip_unmask_pmu_irq,
};

bcm2836의 pmu 인터럽트용 irq_chip 콜백 함수들

 

bcm2836_arm_irqchip_mask_pmu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_mask_pmu_irq(struct irq_data *d)
{
        writel(1 << smp_processor_id(), intc.base + LOCAL_PM_ROUTING_CLR);
}

1개 pmu 인터럽트의 mask 처리 요청을 수행한다.

 

bcm2836_arm_irqchip_unmask_pmu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_unmask_pmu_irq(struct irq_data *d)
{
        writel(1 << smp_processor_id(), intc.base + LOCAL_PM_ROUTING_SET);
}

1개 pmu 인터럽트의 unmask 처리 요청을 수행한다.

 

bcm2836-timer용

bcm2836_arm_irqchip_timer

drivers/irqchip/irq-bcm2836.c

static struct irq_chip bcm2836_arm_irqchip_timer = {
        .name           = "bcm2836-timer",
        .irq_mask       = bcm2836_arm_irqchip_mask_timer_irq,
        .irq_unmask     = bcm2836_arm_irqchip_unmask_timer_irq,
};

bcm2836의 각 코어에 구성된 4개 local 타이머용 irq_chip 콜백 함수들

 

bcm2836_arm_irqchip_mask_timer_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_mask_timer_irq(struct irq_data *d)
{
        bcm2836_arm_irqchip_mask_per_cpu_irq(LOCAL_TIMER_INT_CONTROL0,
                                             d->hwirq - LOCAL_IRQ_CNTPSIRQ,
                                             smp_processor_id());
}

4개의 local timer 인터럽트는 cpu별로 동작하는 per-cpu 타입인데 이 중 현재 cpu에 요청한 타이머 인터럽트(irq_data->hwirq(0~3))를 마스크 처리한다.

  • cpu별 LOCAL_TIMER_INT_CONTROL 레지스터
    • 0x4000_0040 (#0번 core) ~ 0x4000_004c(#3번 core)
  • 4 개 timer 인터럽트 중 요청한 인터럽트(0 ~ 3번 중 하나)를 마스크한다.

 

bcm2836_arm_irqchip_mask_per_cpu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_mask_per_cpu_irq(unsigned int reg_offset,
                                                 unsigned int bit,
                                                 int cpu)
{
        void __iomem *reg = intc.base + reg_offset + 4 * cpu;

        writel(readl(reg) & ~BIT(bit), reg);
}

bcm2836 local 인터럽트 컨트롤러에서 per-cpu 인터럽트에 대한 마스크를 수행한다.

  • rpi2:
    • intc.base는 0x4000_0000
    • reg_offset는 0x40

 

bcm2836_arm_irqchip_unmask_timer_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_unmask_timer_irq(struct irq_data *d)
{
        bcm2836_arm_irqchip_unmask_per_cpu_irq(LOCAL_TIMER_INT_CONTROL0,
                                               d->hwirq - LOCAL_IRQ_CNTPSIRQ,
                                               smp_processor_id());
}

4개의 local timer 인터럽트는 cpu별로 동작하는 per-cpu 타입인데 이 중 현재 cpu에 요청한 타이머 인터럽트(irq_data->hwirq(0~3))를 언마스크 처리한다.

  • cpu별 LOCAL_TIMER_INT_CONTROL 레지스터
    • 0x4000_0040 (#0번 core) ~ 0x4000_004c(#3번 core)
  • 4 개 timer 인터럽트 중 요청한 인터럽트(0 ~ 3번 중 하나)를 클리어한다.

 

bcm2836_arm_irqchip_unmask_per_cpu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_unmask_per_cpu_irq(unsigned int reg_offset,
                                                 unsigned int bit,
                                                 int cpu)
{
        void __iomem *reg = intc.base + reg_offset + 4 * cpu;

        writel(readl(reg) | BIT(bit), reg);
}

bcm2836 local 인터럽트 컨트롤러에서 per-cpu 인터럽트에 대한 언마스크를 수행한다.

 


머신 specific 인터럽트 컨트롤러 초기화

디바이스 트리를 사용하지 않는 경우 다음 코드들이 적용된다.

  • 참고로 ARM32 시스템에서 최근 v4.4 이상 커널들과 모든 ARM64 시스템은 모두 디바이스 트리를 사용하는 방식을 사용한다.

 

bcm2708_init_irq() & bcm2709_init_irq()

arch/arm/mach-bcm2708/bcm2708.c

void __init bcm2708_init_irq(void)
{
        armctrl_init(__io_address(ARMCTRL_IC_BASE), 0, 0, 0);
}

인터럽트 컨트롤러의 base 가상 주소를 인수로 인터럽트 컨트롤러를 초기화한다.

 

arch/arm/mach-bcm2709/bcm2709.c

void __init bcm2709_init_irq(void)
{
        armctrl_init(__io_address(ARMCTRL_IC_BASE), 0, 0, 0);
}

인터럽트 컨트롤러의 base 가상 주소를 인수로 인터럽트 컨트롤러를 초기화한다.

 

rpi vs rpi2 ARM 인터럽트 컨트롤러 주소

BCM2708을 사용하는 rpi와 BCM2709를 사용하는 rpi2는 내부에서 사용하는 디바이스들의 구성은 거의 유사하나 peripheral base 물리 주소가 다르고 이에 따른 가상 주소도 다르다.

  • peripheral base 물리 주소 -> 가상 주소
    • rpi: 0x2000_0000 -> 0xf200_0000
    • rpi2: 0x3f00_0000 -> 0xf300_0000

 

rpi 물리 주소

#define BCM2708_PERI_BASE        0x20000000
#define ARM_BASE                 (BCM2708_PERI_BASE + 0xB000)    /* BCM2708 ARM control block */
#define ARMCTRL_IC_BASE          (ARM_BASE + 0x200)           /* ARM interrupt controller */
  • rpi의 인터럽트 컨트롤러 물리 주소 = 0x2000_b200
  • rpi의 인터럽트 컨트롤러 가상 주소 = 0xf200_b200

 

rpi2 물리 주소

#define BCM2708_PERI_BASE        0x3F000000
#define ARM_BASE                 (BCM2708_PERI_BASE + 0xB000)    /* BCM2708 ARM control block */
#define ARMCTRL_IC_BASE          (ARM_BASE + 0x200)           /* ARM interrupt controller */
  • rpi2의 인터럽트 컨트롤러 물리 주소 = 0x3f00_b200
  • rpi2의 인터럽트 컨트롤러 가상 주소 = 0xf300_b200

 

armctrl_init() – bcm2709 용

arch/arm/mach-bcm2709/armctrl.c

/**
 * armctrl_init - initialise a vectored interrupt controller
 * @base: iomem base address
 * @irq_start: starting interrupt number, must be muliple of 32
 * @armctrl_sources: bitmask of interrupt sources to allow
 * @resume_sources: bitmask of interrupt sources to allow for resume
 */
int __init armctrl_init(void __iomem * base, unsigned int irq_start,
                        u32 armctrl_sources, u32 resume_sources)
{
        unsigned int irq;

        for (irq = 0; irq < BCM2708_ALLOC_IRQS; irq++) {
                unsigned int data = irq;
                if (irq >= INTERRUPT_JPEG && irq <= INTERRUPT_ARASANSDIO)
                        data = remap_irqs[irq - INTERRUPT_JPEG];
                if (irq >= IRQ_ARM_LOCAL_CNTPSIRQ && irq <= IRQ_ARM_LOCAL_TIMER) {
                        irq_set_percpu_devid(irq);
                        irq_set_chip_and_handler(irq, &armctrl_chip, handle_percpu_devid_irq);
                        set_irq_flags(irq, IRQF_VALID | IRQF_NOAUTOEN);
                } else {
                        irq_set_chip_and_handler(irq, &armctrl_chip, handle_level_irq);
                        set_irq_flags(irq, IRQF_VALID | IRQF_PROBE | IRQF_DISABLED);
                }
                irq_set_chip_data(irq, (void *)data);
        }

        armctrl_pm_register(base, irq_start, resume_sources);
        init_FIQ(FIQ_START);
        armctrl_dt_init();
        return 0;
}

bcm2709용 인터럽트 컨트롤러를 초기화하고 각 인터럽트에 대한 핸들러들을 준비한다.

  • 코드 라인 6에서 인터럽트 컨트롤러에 할당된 irq 수 만큼 루프를 돈다.
    • rpi: SPARE_ALLOC_IRQS=394
    • rpi2: SPARE_ALLOC_IRQS=480
  • 코드 라인 8~9에서 GPU용 리눅스 irq를 hwirq로 변환한다.
    • irq 번호가 INTERRUPT_JPEG(74) ~ INTERRUPT_ARASANSDIO(84) 번까지의 irq는 GPU(VC)가 ARM용으로 routing 한 리눅스 irq 번호이므로 기존 GPU(VC)용 hwirq 번호를 알아낸 후 data에 대입한다.
    • GPU(VC) hardware irq 중 11개의 irq는 ARM에서 같이 공유하여 사용하기 위해 ARM pending 레지스터로 라우팅(변환)하여 사용한다.
    • remap_irqs[]
      • INTERRUPT_VC_JPEG(7)
      • INTERRUPT_VC_USB(9)
      • (..생략..)
      • INTERRUPT_VC_ARASANSDIO(62) 까지 총 11개가 등록되어 있다.
  • 코드 라인 10~13에서 irq 번호가 IRQ_ARM_LOCAL_CNTPSIRQ(96) ~ IRQ_ARM_LOCAL_TIMER(107)번 사이의 local 인터럽트인 경우 core별로 전용 사용되므로 per-cpu 디바이스를 취급하는 핸들러를 설정한다.
  • 코드 라인 14~17에서 그 외의 irq 번호인 경우 일반 핸들러를 설정한다.
  • 코드 라인 18에서 hwirq 번호로 chip_data를 구성한다.
  • 코드 라인 21에서 PM(Power Management)를 위해 armctrl_info 구조체에 인터럽트 정보를 대입해둔다.
  • 코드 라인 22에서 FIQ 인터럽트 핸들러를 준비한다.
  • 코드 라인 23에서 Device Tree 스크립트를 통해 해당 인터럽트 컨트롤러 정보를 읽어 irq_domain을 구성한다.
  • 코드 라인 24에서 성공 값 0을 반환한다.

 

다음 그림은 armctrl_init() 함수의 처리 과정을 보여준다.

 

 


Machine용 IRQ Handler (Kernel v4.0 for rpi2)

 

arch_irq_handler_default 매크로(for RPI2)

arch/arm/mach-bcm2709/include/mach/entry-macro.S

/*
 * Interrupt handling.  Preserves r7, r8, r9
 */
        .macro  arch_irq_handler_default
1:	get_irqnr_and_base r0, r2, r6, lr
	.endm

 

IPI(Inter Process Interrupt) 처리

arch/arm/mach-bcm2709/include/mach/entry-macro.S

        .macro  get_irqnr_and_base, irqnr, irqstat, base, tmp

        /* get core number */
        mrc     p15, 0, \base, c0, c0, 5
        ubfx    \base, \base, #0, #2

        /* get core's local interrupt controller */
        ldr     \irqstat, = __io_address(ARM_LOCAL_IRQ_PENDING0)        @ local interrupt source
        add     \irqstat, \irqstat, \base, lsl #2
        ldr     \tmp, [\irqstat]
#ifdef CONFIG_SMP
        /* test for mailbox0 (IPI) interrupt */
        tst     \tmp, #0x10
        beq     1030f

        /* get core's mailbox interrupt control */
        ldr     \irqstat, = __io_address(ARM_LOCAL_MAILBOX0_CLR0)       @ mbox_clr
        add     \irqstat, \irqstat, \base, lsl #4
        ldr     \tmp, [\irqstat]
        clz     \tmp, \tmp
        rsb     \irqnr, \tmp, #31
        mov     \tmp, #1
        lsl     \tmp, \irqnr
        str     \tmp, [\irqstat]  @ clear interrupt source
        dsb
        mov     r1, sp
        adr     lr, BSYM(1b)
        b       do_IPI
#endif

 

pending 레지스터를 읽어 IPI 처리 요청이 있는 경우 mailbox0를 읽어 인터럽트 소스를 알아와서 모든 설정된 IPI 처리를 한다. 처리 순서는 높은 IPI 번호부터 처리한다. (irqnr=r0, irqstat=r2, base=r6, tmp=lr)

  • 코드 라인 4~10에서 cpu 번호를 구한 후 그 cpu에 해당하는 pending 레지스터 값을 읽어와서 \tmp에 대입한다.
    • 예) cpu #3에 대한 레지스터 가상 주소: ARM_LOCAL_IRQ_PENDING0(0xf400_0060) + cpu(3) * 4 = 0xf400_006c
  • 코드 라인 13~14에서 IPI(mailbox0) 인터럽트 요청이 아닌 경우 1030 레이블로 이동한다.
    • pending 레지스터의 0x10 비트: mailbox0
  • 코드 라인 17~19에서 cpu에 해당하는 mailbox0 값을 읽어와서 \tmp에 대입한다.
  • 코드 라인 20~21에서 가장 좌측에 설정된 인터럽트 비트를 \irqnr에 대입한다. (IPI는 역순으로 우선 처리된다.)
  • 코드 라인 22~24에서 클리어하고자 하는 IPI 인터럽트 비트만 설정하여 읽어왔던 mailbox0에 저장한다.
    • clz: 좌측부터 0으로 시작하는 비트 수
    • mailbox는 str 명령을 통하여 값이 저장되는 것이 아니라 값에 해당하는 인터럽트 소스들을 클리어 한다.
  • 코드 라인 26~28에서 두 번째 인수 r1에 스택에 있는 pt_regs를 대입하고, 복귀 주소로 arch_irq_handler_default 매크로 안에 있는 get_irqnr_and_base 호출 위치를 설정한 다음 IPI 처리를 위해 do_IPI() 함수를 호출한다.
    • IPI 처리가 다 완료될 때 까지 루프를 돈다.
    • 참고: do_IPI() | 문c

 

 

 

ARM(GPU) 및 ARM 인터럽트 처리
1030:
        /* check gpu interrupt */
        tst     \tmp, #0x100
        beq     1040f

        ldr     \base, =IO_ADDRESS(ARMCTRL_IC_BASE)
        /* get masked status */
        ldr     \irqstat, [\base, #(ARM_IRQ_PEND0 - ARMCTRL_IC_BASE)]
        mov     \irqnr, #(ARM_IRQ0_BASE + 31)
        and     \tmp, \irqstat, #0x300           @ save bits 8 and 9
        /* clear bits 8 and 9, and test */
        bics    \irqstat, \irqstat, #0x300
        bne     1010f

        tst     \tmp, #0x100
        ldrne   \irqstat, [\base, #(ARM_IRQ_PEND1 - ARMCTRL_IC_BASE)]
        movne   \irqnr, #(ARM_IRQ1_BASE + 31)
        @ Mask out the interrupts also present in PEND0 - see SW-5809
        bicne   \irqstat, #((1<<7) | (1<<9) | (1<<10))
        bicne   \irqstat, #((1<<18) | (1<<19))
        bne     1010f

        tst     \tmp, #0x200
        ldrne   \irqstat, [\base, #(ARM_IRQ_PEND2 - ARMCTRL_IC_BASE)]
        movne   \irqnr, #(ARM_IRQ2_BASE + 31)
        @ Mask out the interrupts also present in PEND0 - see SW-5809
        bicne   \irqstat, #((1<<21) | (1<<22) | (1<<23) | (1<<24) | (1<<25))
        bicne   \irqstat, #((1<<30))
        beq     1020f
1010:
        @ For non-zero x, LSB(x) = 31 - CLZ(x^(x-1))
        sub     \tmp, \irqstat, #1
        eor     \irqstat, \irqstat, \tmp
        clz     \tmp, \irqstat
        sub     \irqnr, \tmp
        b       1050f

 

pending #0 레지스터를 읽어 ARM(GPU) #1, ARM(GPU)#2, ARM #0 인터럽트 순서대로 발생한 인터럽트 소스의 ISR을 수행한다.

  • 예) ARM(GPU) #1에 배치된 arm dma #0 인터럽트가 요청된 경우
    • pending 0 레지스터 값=0x100, pending 1 레지스터 값=0x1_0000 (bit16), 최종 인터럽트 번호 \irqnr=16
  • 예) ARM(GPU) #2에 배치한 arm gpio 인터럽트가 요청된 경우
    • pending 0 레지스터 값=0x200, pending 2 레지스터 값=0x10_0000 (bit20), 최종 인터럽트 번호 \irqnr=52
  • 예) ARM #0에 배치한 arm uart 인터럽트가 요청된 경우
    • pending 0 레지스터 값=0x8_0000 (bit19), 최종 인터럽트 번호 \irqnr=83

 

  • 코드 라인 3~4에서 ARM(GPU) 인터럽트 처리 요청이 없으면 1040 레이블로 이동한다.
  • 코드 라인 6~8에서 pending #0 레지스터 값을 읽어 /irqstat에 저장한다.
    • ARM 인터럽트 컨트롤러 가상 주소를 구해 \base에 저장한다.
      • =IO_ADDRESS(0x3f00_0000(기본 주소) + 0xb000(peri offset) + 0x200(인터럽트 컨트롤러 offset) = 0xf300_b200
  • 코드 라인 9에서 ARM #0이 담당하는 IRQ 끝 번호 값(95)을 \irqnr에 대입한다.
  • 코드 라인 10~13에서 pending #0 레지스터에서 읽었던 /irqstat에서 ARM #1(GPU #1) 또는 ARM #2(GPU #2)에 해당하는 비트를 클리어한다. 클리어 전에도 설정된 적 없으면 ARM #0 인터럽트를 처리하기 위해 1010 레이블로 이동한다.
  • 코드 라인 15~21에서 ARM #1(GPU #1)에 해당하는 경우 pending #1 레지스터 값을 읽어 /irqstat에 저장하되 VideoCore에서 사용하는 인터럽트에 해당하는 7, 9, 10, 18, 19번 비트들을 제거한다. IRQ #1(GPU #1)의 인터럽트 끝 번호(31)를 \irqnr에 대입한 후 해당 인터럽트 수행을 위해 1010 레이블로 이동한다.
  • 코드 라인 23~29에서 ARM #2(GPU #2)에 해당하는 경우 pending #2 레지스터 값을 읽어 /irqstat에 저장하되 VideoCore에서 사용하는 인터럽트에 해당하는21~25, 30번 비트들을 제거한다. IRQ #2(GPU #2)의 인터럽트 끝 번호(63)를 \irqnr에 대입하고 해당 인터럽트 수행을 위해 계속 아래 1010 레이블로 진행한다.
  • 코드 라인 32~36에서 처리할 인터럽트 끝 번호(irq0=95, irq1=31, ir12=63, local irq=127)에서 \irqstat의 첫 인터럽트 비트에 해당하는 번호를 뺀 후 해당 인터럽트 수행을 위해 1050 레이블로 이동한다.

 

Local Interrupt 처리
1040:
        cmp     \tmp, #0
        beq     1020f

        /* handle local (e.g. timer) interrupts */
        @ For non-zero x, LSB(x) = 31 - CLZ(x^(x-1))
        mov     \irqnr, #(ARM_IRQ_LOCAL_BASE + 31)
        sub     \irqstat, \tmp, #1
        eor     \irqstat, \irqstat, \tmp
        clz     \tmp, \irqstat
        sub     \irqnr, \tmp
1050:
        mov     r1, sp
        @
        @ routine called with r0 = irq number, r1 = struct pt_regs *
        @
        adr     lr, BSYM(1b)
        b       asm_do_IRQ

1020:   @ EQ will be set if no irqs pending
        .endm

local ARM 인터럽트 처리요청이 있는 경우 발생한 인터럽트 소스의 ISR을 수행한다.

  • 예) Local ARM 인터럽트에 배치한 arm timer 인터럽트가 요청된 경우
    • local interrupt controller 레지스터 값=0x8, 최종 인터럽트 번호 \irqnr=99

 

  • 코드 라인 2~3에서 pending 레지스터 값이 0인 경우 더이상 처리할 인터럽트가 없으므로 종료한다.
  • 코드 라인 7~11에서 Local ARM이 처리하는 끝 번호(127) 값을 \irqnr에 대입한다.  끝 번호(127)에서 /tmp 인터럽트 비트들 중 가장 마지막 비트에 해당하는 번호를  뺀다
  • 코드 라인 13~18에서 r1 레지스터에 pt_regs 주소를 대입하고, lr에 이 매크로(get_irqnr_and_base)를 호출하는 곳을 담고 ISR을 호출한다.

 


FIQ 초기화

init_FIQ()

arch/arm/kernel/fiq.c

void __init init_FIQ(int start)
{
        unsigned offset = FIQ_OFFSET;
        dfl_fiq_insn = *(unsigned long *)(0xffff0000 + offset);
        get_fiq_regs(&dfl_fiq_regs);
        fiq_start = start;
}

default FIQ 인터럽트 처리기를 백업하기 위해 fiq 벡터 값과 fiq 모드에서 사용하는 레지스터들 일부(r8 ~ r12, sp, lr)를 백업해둔다.

  • fiq가 필요한 owner task에서 핸들러를 준비하여 요청/해제등을 수행 시 파괴된 레지스터와 fiq 벡터들을 복원시 사용하기 위해 백업해둔다.
    • STM 및 LDM 수행은 보통 atomic 하게 수행되지만 cpu가 low interrupt latency 모드를 사용하는 경우 STM과 LDM 명령이 atomic하게 이루어지지 않아 STM 도중에 fiq exception 되는 경우 완전하게 스택에 기록하지 않는 문제가 발생하고 fiq 인터럽트 서비스 루틴 수행 후 다시 원래 모드 루틴으로 복귀할 때 LDM으로 읽은 값(최종 값이 보통 pc)에 가베지가 로드되어 문제가 발생할 수 있다. 이런 경우를 위해 fiq 모드에서 사용하였던 default 인터럽트 핸들러용 레지스터들을 백업해두고 이를 복원하는 용도로 사용한다.
    • fiq는 irq에 비해 약 10 ~ 20여배 latency가 작다. (10 ~ 20 ns vs 수 백 ns)
    • 참고: [ARM] 3256/1: Make the function-returning ldm’s use sp as the base register
  • rpi2 예)
    • “usb_fiq” 라는 이름의 usb 드라이버에서 fiq를 사용한다.
    • hcd_init_fiq() – drivers/usb/host/dwc_otg/dwc_otg_hcd_linux.c

 

  • 코드 라인 3~4에서 fiq에 해당하는 exception 벡터에 기록된 명령을 읽어와서 전역 변수 dfl_fiq_insn에 저장한다.
  • 코드 라인 5에서 fiq 모드로 진입한 후 레지스터 백업 장소 dfl_fiq_regs->r8 위치에 레지스터 r8 ~ r12, sp, lr을 저장하고 다시 svc 모드로 돌아온다.
  • 코드 라인 6에서 fiq_start에 hw irq 시작 번호를 대입한다.
    • rpi: 85
    • rpi2: 128

 

ENTRY(__get_fiq_regs)
        mov     r2, #PSR_I_BIT | PSR_F_BIT | FIQ_MODE
        mrs     r1, cpsr
        msr     cpsr_c, r2      @ select FIQ mode
        mov     r0, r0          @ avoid hazard prior to ARMv4
        stmia   r0!, {r8 - r12}
        str     sp, [r0], #4
        str     lr, [r0]
        msr     cpsr_c, r1      @ return to SVC mode 
        mov     r0, r0          @ avoid hazard prior to ARMv4
        ret     lr
ENDPROC(__get_fiq_regs)

FIQ 모드로 진입한 후 인수 r0 레지스터가 가리키는 주소에 현재 레지스터 r8 ~ r12, sp, lr을 저장하고 다시 SVC 모드로 돌아온다.

 

Device Tree에서 인터럽트 컨트롤러 노드 정보를 읽어 irq domain 생성

armctrl_dt_init()

arch/arm/mach-bcm2709/armctrl.c

void __init armctrl_dt_init(void)
{
        struct device_node *np;
        struct irq_domain *domain;

        np = of_find_compatible_node(NULL, NULL, "brcm,bcm2708-armctrl-ic");
        if (!np)
                return;

        domain = irq_domain_add_legacy(np, BCM2708_ALLOC_IRQS,
                                        IRQ_ARMCTRL_START, 0,
                                        &armctrl_ops, NULL);
        WARN_ON(!domain);
}

디바이스 트리 스크립트에 설정된 인터럽트 컨트롤러 명이 “brcm,bcm2708-armctrl-ic”인 경우 irq_domain을 추가하고 legacy 방법으로 domain을 최대 할당 인터럽트 수 만큼 지정한다.

  • 최대 할당 인터럽트 수
    • rpi: 394
    • rpi2: 480

 

참고

 

Interrupts -10- (irq partition)

<kernel v5.4>

Interrupts -10- (irq partition)

PPI는 per-cpu 인터럽트들은 irq번호가 동일하다. 그런데 클러스터마다 인터럽트 트리거 타입 설정 및 인터럽트 처리 방법이 다른 경우가 발생하여 이를 처리하기 위해 클러스터 단위로 파티션을 나누어 처리하였다.

 

per-cpu 인터럽트(PPI)

다음 그림은 PPI에 해당하는 per-cpu 인터럽트와 SPI에 해당하는 일반 인터럽트가 처리될 때의 흐름을 보여준다.

  • 1개의 per-cpu 인터럽트는 cpu 수 만큼 라인을 가지며 각 cpu에 라우팅되고, 하나의 irq 디스크립터에서 처리된다.

 

다음 그림은 GIC v3 드라이버가 PPI 및 SPI에서 수신된 인터럽트를 처리할 때 사용하는 도메인과 irq_chip의 operations 함수들을 보여준다.

 

IRQ Partition

다음 그림은 per-cpu 인터럽트를 처리할 때 PPI용 irq 아래에 하위 파티션 도메인 및 irq 디스크립터들을 구성하고, 호출하는 과정을 보여준다.

 

다음 그림은 per-cpu 인터럽트를 처리 하기 위해 생성된 파티션 디스크립터의 내부 구조를 보여준다.

  • GIC v3 드라이버는 초기화 시 모든 PPI 수 만큼 파티션 디스크립터를 생성한다. 그러나 실제 운영은 파티션 요청한 디바이스에 한하여 운영된다.

 

다음은 디바이스 트리에서 일반 PPI를 사용하는 디바이스와 irq 파티션을 사용하는 디바이스의 구성을 보여준다.

  • irq 파티션을 사용하려면 인터럽트 컨트롤러 노드내에 ppi-partitions을 구성해야 한다.
  • big/little에 사용하는 pmu 디바이스가 각각의 칩에서 별도의 인터럽트 핸들러를 동작시키기위해 irq 파티션을 사용한다.
    • 디바이스가 irq 파티션을 사용하려면 interrupts 속성에서 4개 이상의 인자가 필요하고 4 번째 인자는 파티션을 가리키는 phandle을 사용해야 한다. (irq 파티션을 사용하지 않을 때 4 번째 인자는 0이다)

 

per-cpu 인터럽트를 파티션으로 나눈 인터럽트는 cascaded(chained) irq 연결에 의해 파티션마다 별도의 처리가 가능하다.

 

다음은 QEMU/KVM를 사용한 Guest OS에서 big/little 코어 각각 1나씩 irq-partition을 만든 경우이다.

GICv3: 256 SPIs implemented
GICv3: 0 Extended SPIs implemented
GICv3: Distributor has no Range Selector support
GICv3: 16 PPIs implemented
GICv3: no VLPI support, no direct LPI support
GICv3: CPU0: found redistributor 0 region 0:0x00000000080a0000
GICv3: GIC: PPI partition interrupt-partition-0[0] { /cpus/cpu@0[0] }
GICv3: GIC: PPI partition interrupt-partition-1[1] { /cpus/cpu@1[1] }

 

arm-pmu 디바이스가 파티션되어 운영되는 모습을 알 수 있다.

  • 하위 파티션 도메인에서 생성된 hwirq는 순서대로 0부터 시작한다.
  • “GICv3-23” 출력된 메시지는 INTID#23 이라는 의미이며, PPI#7과 동일하다.
$ cat /proc/interrupts
           CPU0       CPU1
 12:       1822       1678     GICv3  27 Level     arch_timer
 47:       1527          0     GICv3  78 Edge      virtio0
 48:         17          0     GICv3  79 Edge      virtio1
 50:          0          0     GICv3  34 Level     rtc-pl031
 51:         41          0     GICv3  33 Level     uart-pl011
 52:          0          0  GICv3-23   0 Level     arm-pmu
 53:          0          0  GICv3-23   1 Level     arm-pmu
 54:          0          0  9030000.pl061   3 Edge      GPIO Key Poweroff
IPI0:       854        798       Rescheduling interrupts
IPI1:         8        800       Function call interrupts
IPI2:         0          0       CPU stop interrupts
IPI3:         0          0       CPU stop (for crash dump) interrupts
IPI4:         0          0       Timer broadcast interrupts
IPI5:         0          0       IRQ work interrupts
IPI6:         0          0       CPU wake-up interrupts

 


PPI 파티션들 초기화

gic_populate_ppi_partitions()

drivers/irqchip/irq-gic-v3.c -1/2-

/* Create all possible partitions at boot time */
static void __init gic_populate_ppi_partitions(struct device_node *gic_node)
{
        struct device_node *parts_node, *child_part;
        int part_idx = 0, i;
        int nr_parts;
        struct partition_affinity *parts;

        parts_node = of_get_child_by_name(gic_node, "ppi-partitions");
        if (!parts_node)
                return;

        gic_data.ppi_descs = kcalloc(gic_data.ppi_nr, sizeof(*gic_data.ppi_descs), GFP_KERNEL);
        if (!gic_data.ppi_descs)
                return;

        nr_parts = of_get_child_count(parts_node);

        if (!nr_parts)
                goto out_put_node;

        parts = kcalloc(nr_parts, sizeof(*parts), GFP_KERNEL);
        if (WARN_ON(!parts))
                goto out_put_node;

        for_each_child_of_node(parts_node, child_part) {
                struct partition_affinity *part;
                int n;

                part = &parts[part_idx];

                part->partition_id = of_node_to_fwnode(child_part);

                pr_info("GIC: PPI partition %pOFn[%d] { ",
                        child_part, part_idx);

                n = of_property_count_elems_of_size(child_part, "affinity",
                                                    sizeof(u32));
                WARN_ON(n <= 0);

                for (i = 0; i < n; i++) {
                        int err, cpu;
                        u32 cpu_phandle;
                        struct device_node *cpu_node;

                        err = of_property_read_u32_index(child_part, "affinity",
                                                         i, &cpu_phandle);
                        if (WARN_ON(err))
                                continue;

                        cpu_node = of_find_node_by_phandle(cpu_phandle);
                        if (WARN_ON(!cpu_node))
                                continue;

                        cpu = of_cpu_node_to_id(cpu_node);
                        if (WARN_ON(cpu < 0))
                                continue;

                        pr_cont("%pOF[%d] ", cpu_node, cpu);

                        cpumask_set_cpu(cpu, &part->mask);
                }

                pr_cont("}\n");
                part_idx++;
        }

PPI를 위한 irq 파티션을 생성한다.

  • 코드 라인 9~11에서 디바이스 트리에서 ppi-partitions 노드를 찾을 수 없으면 함수를 빠져나간다.
  • 코드 라인 13~15에서 ppi 수만큼 partition_desc 구조체를 할당한다.
  • 코드 라인 17~20에서 child 노드 수를 읽어와 nr_parts에 대입한다.
  • 코드 라인 22~24에서 파티션 수(nr_parts)만큼 partition_affinity 구조체를 할당하고 디바이스 트리의 파티션 정보를 출력한다.
    • 다음은 두 개의 파티션을 사용하고 있는 rock960 보드의 관련 커널 로그를 출력한 예이다.
      • “GIC: PPI partition interrupt-partition-0[0] { /cpus/cpu@0[0] /cpus/cpu@1[1] /cpus/cpu@2[2] /cpus/cpu@3[3] }”
      • “GIC: PPI partition interrupt-partition-1[1] { /cpus/cpu@100[4] /cpus/cpu@101[5] }”
  • 코드 라인 26~66에서 파티션 수만큼 순회하며 디바이스 트리의 “affinity” 속성에 포함된 cpu 노드의 cpu 번호를 partition_affinity 구조체에 설정한다.

 

drivers/irqchip/irq-gic-v3.c -2/2-

        for (i = 0; i < gic_data.ppi_nr; i++) {
                unsigned int irq;
                struct partition_desc *desc;
                struct irq_fwspec ppi_fwspec = {
                        .fwnode         = gic_data.fwnode,
                        .param_count    = 3,
                        .param          = {
                                [0]     = GIC_IRQ_TYPE_PARTITION,
                                [1]     = i,
                                [2]     = IRQ_TYPE_NONE,
                        },
                };

                irq = irq_create_fwspec_mapping(&ppi_fwspec);
                if (WARN_ON(!irq))
                        continue;
                desc = partition_create_desc(gic_data.fwnode, parts, nr_parts,
                                             irq, &partition_domain_ops);
                if (WARN_ON(!desc))
                        continue;

                gic_data.ppi_descs[i] = desc;
        }

out_put_node:
        of_node_put(parts_node);
}
  • 코드 라인 1~23에서 ppi 수 만큼 순회하며 클러스터 단위의 파티션 별로 구성되어 irq를 나누어 처리하기 위해 irq 도메인 및 irq chip 등을 파티션 구성한다.
    • 참고로 3개의 인자로 생성하여 매핑한 PPI 파티션 디스크립터들을 생성하지만, 하위 파티션용 도메인과 하위 irq 디스크립터가 생성되지는 않는다.

 


파티션 디스크립터 생성

partition_create_desc()

drivers/irqchip/irq-partition-percpu.c

struct partition_desc *partition_create_desc(struct fwnode_handle *fwnode,
                                             struct partition_affinity *parts,
                                             int nr_parts,
                                             int chained_irq,
                                             const struct irq_domain_ops *ops)
{
        struct partition_desc *desc;
        struct irq_domain *d;

        BUG_ON(!ops->select || !ops->translate);

        desc = kzalloc(sizeof(*desc), GFP_KERNEL);
        if (!desc)
                return NULL;

        desc->ops = *ops;
        desc->ops.free = partition_domain_free;
        desc->ops.alloc = partition_domain_alloc;

        d = irq_domain_create_linear(fwnode, nr_parts, &desc->ops, desc);
        if (!d)
                goto out;
        desc->domain = d;

        desc->bitmap = kcalloc(BITS_TO_LONGS(nr_parts), sizeof(long),
                               GFP_KERNEL);
        if (WARN_ON(!desc->bitmap))
                goto out;

        desc->chained_desc = irq_to_desc(chained_irq);
        desc->nr_parts = nr_parts;
        desc->parts = parts;

        return desc;
out:
        if (d)
                irq_domain_remove(d);
        kfree(desc);

        return NULL;
}

하나의 ppi에 해당하는 파티션 디스크립터를 할당하고 구성한다.

  • 코드 라인 12~18에서 파티션 디스크립터(partition_desc 구조체)를 할당하고 인자로 전달받은 @ops와 파티션용 도메인 할당/해제 함수를 지정한다.
    • partition_domain_alloc() & partition_domain_free()
  • 코드 라인 20~23에서 파티션 수 만큼 리니어 irq를 관리하는 도메인을 생성한다.
  • 코드 라인 25~28에서 파티션을 관리하기 위한 비트맵을 할당한다.
  • 코드 라인 30~32에서 파티션 디스크립터에 @chined_irq와 파티션 수 및 할당한 파티션 affinity를 지정한다.
  • 코드 라인 34에서 할당하여 구성한 파티션 디스크립터를 반환한다.

 

파티션용 irq 도메인 할당

다음 파티션용 도메인이 생성되려면 디바이스 트리에서 디바이스가 interrupts 속성에서 파티션을 지정해야 한다.

  • 예) interrupts = <1 7 4 &ppi_cluster0>;

 

partition_domain_alloc()

drivers/irqchip/irq-partition-percpu.c

static int partition_domain_alloc(struct irq_domain *domain, unsigned int virq,
                                  unsigned int nr_irqs, void *arg)
{
        int ret;
        irq_hw_number_t hwirq;
        unsigned int type;
        struct irq_fwspec *fwspec = arg;
        struct partition_desc *part;

        BUG_ON(nr_irqs != 1);
        ret = domain->ops->translate(domain, fwspec, &hwirq, &type);
        if (ret)
                return ret;

        part = domain->host_data;

        set_bit(hwirq, part->bitmap);
        irq_set_chained_handler_and_data(irq_desc_get_irq(part->chained_desc),
                                         partition_handle_irq, part);
        irq_set_percpu_devid_partition(virq, &part->parts[hwirq].mask);
        irq_domain_set_info(domain, virq, hwirq, &partition_irq_chip, part,
                            handle_percpu_devid_irq, NULL, NULL);
        irq_set_status_flags(virq, IRQ_NOAUTOEN);

        return 0;
}

하위 파티션용 @virq를 파티션 도메인의 매치된 hwirq를 설정하고, 상위 도메인에 체인 연결한다.

  • 코드 라인 11~13에서 fwspec 인자를 가지고 요청한 하위 irq 도메인의 (*translate) 후크 함수를 호출하여 hwirq 및 타입을 알아온다.
    • 파티션용 hwirq의 시작번호는 0번부터 시작한다.
  • 코드 라인 15에서 하위 파티션 @domain의 host_data에 저장된 파티션 디스크립터를 알아온다.
  • 코드 라인 17에서 해당 파티션에 대한 비트맵을 설정한다.
  • 코드 라인 18~19에서 해당 파티션에 대한 chained 핸들러에 partition_handle_irq() 함수를 지정한다.
    • 파티션에 대한 상위 irq 디스크립터는 part->chained_desc 이다.
  • 코드 라인 20에서 하위 @virq에 해당하는 irq 디스크립터의 percpu 기능을 설정한다.
  • 코드 라인 21~22에서 파티션 @domain의 하위 @virq에 관련 hwriq 및 파티션용 irq_chip 정보, 파티션 디스크립터 및 percpu 인터럽트 핸들러들을 설정한다.
  • 코드 라인 23에서 하위 @virq에 해당하는 디스크립터의 플래그에 IRQ_NOAUTOEN 플래그를 설정한다.

 

irq_set_percpu_devid_partition()

kernel/irq/irqdesc.c

int irq_set_percpu_devid_partition(unsigned int irq,
                                   const struct cpumask *affinity)
{
        struct irq_desc *desc = irq_to_desc(irq);

        if (!desc)
                return -EINVAL;

        if (desc->percpu_enabled)
                return -EINVAL;

        desc->percpu_enabled = kzalloc(sizeof(*desc->percpu_enabled), GFP_KERNEL);

        if (!desc->percpu_enabled)
                return -ENOMEM;

        if (affinity)
                desc->percpu_affinity = affinity;
        else
                desc->percpu_affinity = cpu_possible_mask;

        irq_set_percpu_devid_flags(irq);
        return 0;
}

@irq에 해당하는 irq 디스크립터에 percpu 기능을 설정한다.

  • 코드 라인 4~10에서 @irq 번호에 해당하는 irq 디스크립터에서 percpu_enabled가 이미 설정된 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 12~15에서 percpu_enabled에 cpu 비트마스크를 할당한다.
  • 코드 라인 17~20에서 irq 디스크립터에 percpu용 affinity를 설정한다. 인자로 받은 @affinity가 주어진 경우 이 값을 사용하고, 주어지지 않은 경우 모든 possible cpu를 지정한다.
  • 코드 라인 22에서 irq 디스크립터에 percpu 기능 활성화와 관련된 플래그들을 설정한다.

 

irq_set_percpu_devid_flags()

include/linux/irq.h

static inline void irq_set_percpu_devid_flags(unsigned int irq)
{
        irq_set_status_flags(irq,
                             IRQ_NOAUTOEN | IRQ_PER_CPU | IRQ_NOTHREAD |
                             IRQ_NOPROBE | IRQ_PER_CPU_DEVID);
}

@irq 번호에 해당하는 디스크립터에 percpu 기능 활성화와 관련된 플래그들을 설정한다.

 

파티션용 인터럽트 핸들러

partition_handle_irq()

drivers/irqchip/irq-partition-percpu.c

static void partition_handle_irq(struct irq_desc *desc)
{
        struct partition_desc *part = irq_desc_get_handler_data(desc);
        struct irq_chip *chip = irq_desc_get_chip(desc);
        int cpu = smp_processor_id();
        int hwirq;

        chained_irq_enter(chip, desc);

        for_each_set_bit(hwirq, part->bitmap, part->nr_parts) {
                if (partition_check_cpu(part, cpu, hwirq))
                        break;
        }

        if (unlikely(hwirq == part->nr_parts)) {
                handle_bad_irq(desc);
        } else {
                unsigned int irq;
                irq = irq_find_mapping(part->domain, hwirq);
                generic_handle_irq(irq);
        }

        chained_irq_exit(chip, desc);
}

상위 irq에 등록된 파티션용 핸들러이다. irq 디스크립터(@desc)를 사용하여 해당 파티션을 찾고 그 파티션에 등록된 irq 디스크립터의 핸들러 함수를 호출하여 수행한다.

  • 코드 라인 3에서 irq 디스크립터의 핸들러 데이터에 저장된 파티션 디스크립터를 알아온다.
  • 코드 라인 4에서 irq 디스크립터의 irq 칩을 알아온다.
  • 코드 라인 8에서 chained irq의 시작을 선언한다.
  • 코드 라인 10~13에서 파티션 디스크립터의 비트맵을 순회하며 설정된 비트로부터 hwirq를  알아온다.
    • 두 개의 파티션이 사용되는 경우 hwirq=0~1이다.
  • 코드 라인 15~21에서 파티션 디스크립터에 소속된 파티션 도메인에서 hwirq에 해당하는 irq를 찾아 해당 핸들러 함수를 호출한다.
    • PPI 인터럽트 핸들러인 handle_percpu_devid_irq() 함수가 호출된다.
  • 코드 라인 23에서 chained irq의 끝을 선언한다.

 

partition_check_cpu()

drivers/irqchip/irq-partition-percpu.c

static bool partition_check_cpu(struct partition_desc *part,
                                unsigned int cpu, unsigned int hwirq)
{
        return cpumask_test_cpu(cpu, &part->parts[hwirq].mask);
}

파티션 디스크립터의 @hwirq에 해당하는 파티션에 요청한 @cpu가 포함되어 있는지 여부를 반환한다.

 


파티션용 irq_chip Operations

partition_irq_chip

drivers/irqchip/irq-partition-percpu.c

static struct irq_chip partition_irq_chip = {
        .irq_mask               = partition_irq_mask,
        .irq_unmask             = partition_irq_unmask,
        .irq_set_type           = partition_irq_set_type,
        .irq_get_irqchip_state  = partition_irq_get_irqchip_state,
        .irq_set_irqchip_state  = partition_irq_set_irqchip_state,
        .irq_print_chip         = partition_irq_print_chip,
};

하위 irq 디스크립터에 연동되어 사용되는 irq 칩에 대한 operations 함수이다.

 

partition_irq_mask()

drivers/irqchip/irq-partition-percpu.c

static void partition_irq_mask(struct irq_data *d)
{
        struct partition_desc *part = irq_data_get_irq_chip_data(d);
        struct irq_chip *chip = irq_desc_get_chip(part->chained_desc);
        struct irq_data *data = irq_desc_get_irq_data(part->chained_desc);

        if (partition_check_cpu(part, smp_processor_id(), d->hwirq) &&
            chip->irq_mask)
                chip->irq_mask(data);
}

하위 irq 디스크립터 @d에 연동된 상위 chained-irq 디스크립터를 사용해 irq mask를 수행한다.

 

partition_irq_unmask()

drivers/irqchip/irq-partition-percpu.c

static void partition_irq_unmask(struct irq_data *d)
{
        struct partition_desc *part = irq_data_get_irq_chip_data(d);
        struct irq_chip *chip = irq_desc_get_chip(part->chained_desc);
        struct irq_data *data = irq_desc_get_irq_data(part->chained_desc);

        if (partition_check_cpu(part, smp_processor_id(), d->hwirq) &&
            chip->irq_unmask)
                chip->irq_unmask(data);
}

하위 irq 디스크립터 @d에 연동된 상위 chained-irq 디스크립터를 사용해 irq unmask를 수행한다.

 

partition_irq_set_type()

drivers/irqchip/irq-partition-percpu.c

static int partition_irq_set_type(struct irq_data *d, unsigned int type)
{
        struct partition_desc *part = irq_data_get_irq_chip_data(d);
        struct irq_chip *chip = irq_desc_get_chip(part->chained_desc);
        struct irq_data *data = irq_desc_get_irq_data(part->chained_desc);

        if (chip->irq_set_type)
                return chip->irq_set_type(data, type);

        return -EINVAL;
}

하위 irq 디스크립터 @d에 연동된 상위 chained-irq 디스크립터를 사용해 irq type을 지정한다.

 

partition_irq_get_irqchip_state()

drivers/irqchip/irq-partition-percpu.c

static int partition_irq_get_irqchip_state(struct irq_data *d,
                                           enum irqchip_irq_state which,
                                           bool *val)
{
        struct partition_desc *part = irq_data_get_irq_chip_data(d);
        struct irq_chip *chip = irq_desc_get_chip(part->chained_desc);
        struct irq_data *data = irq_desc_get_irq_data(part->chained_desc);

        if (partition_check_cpu(part, smp_processor_id(), d->hwirq) &&
            chip->irq_get_irqchip_state)
                return chip->irq_get_irqchip_state(data, which, val);

        return -EINVAL;
}

하위 irq 디스크립터 @d에 연동된 상위 chained-irq 디스크립터를 사용해 irqchip state를 알아온다.

 

partition_irq_set_irqchip_state()

drivers/irqchip/irq-partition-percpu.c

static int partition_irq_set_irqchip_state(struct irq_data *d,
                                           enum irqchip_irq_state which,
                                           bool val)
{
        struct partition_desc *part = irq_data_get_irq_chip_data(d);
        struct irq_chip *chip = irq_desc_get_chip(part->chained_desc);
        struct irq_data *data = irq_desc_get_irq_data(part->chained_desc);

        if (partition_check_cpu(part, smp_processor_id(), d->hwirq) &&
            chip->irq_set_irqchip_state)
                return chip->irq_set_irqchip_state(data, which, val);

        return -EINVAL;
}

하위 irq 디스크립터 @d에 연동된 상위 chained-irq 디스크립터를 사용해 irqchip state를 설정한다.

 

partition_irq_print_chip()

drivers/irqchip/irq-partition-percpu.c

static void partition_irq_print_chip(struct irq_data *d, struct seq_file *p)
{
        struct partition_desc *part = irq_data_get_irq_chip_data(d);
        struct irq_chip *chip = irq_desc_get_chip(part->chained_desc);
        struct irq_data *data = irq_desc_get_irq_data(part->chained_desc);

        seq_printf(p, " %5s-%lu", chip->name, data->hwirq);
}

하위 irq 디스크립터 @d에 연동된 상위 chained-irq 디스크립터이 사용중인 irq chip 명과 hwirq를 출력한다.

  • 예) “GICv3-23”
    • INTID#23 -> PPI#7

 


GIC v3 인터럽트 컨트롤러

partition_domain_ops

drivers/irqchip/irq-gic-v3.c

static const struct irq_domain_ops partition_domain_ops = {
        .translate = partition_domain_translate,
        .select = gic_irq_domain_select,
};

파티션 도메인용 opeartions이다.

  • 하위 도메인으로 사용된다.

 

파티션용 irq 도메인 변환

partition_domain_translate()

drivers/irqchip/irq-partition-percpu.c

static int partition_domain_translate(struct irq_domain *d,
                                      struct irq_fwspec *fwspec,
                                      unsigned long *hwirq,
                                      unsigned int *type)
{
        struct device_node *np;
        int ret;

        if (!gic_data.ppi_descs)
                return -ENOMEM;

        np = of_find_node_by_phandle(fwspec->param[3]);
        if (WARN_ON(!np))
                return -EINVAL;

        ret = partition_translate_id(gic_data.ppi_descs[fwspec->param[1]],
                                     of_node_to_fwnode(np));
        if (ret < 0)
                return ret;

        *hwirq = ret;
        *type = fwspec->param[2] & IRQ_TYPE_SENSE_MASK;

        return 0;
}

파티션 도메인(@d)에사 @fwspec로 irq 디스크립터를 찾아 해당 hwirq 및 type을 알아온다. 성공 시 0을 반환한다.

  • 코드 라인 9~10에서 ppi용 파티션 디스크립터들이 만들어지지 않은 경우 함수를 빠져나간다.
  • 코드 라인 12~14에서 @fwspec으로 전달받은 4번째 인자에 phandle 해당하는 노드를 찾아온다.
    • 찾은 디바이스 노드가 파티션을 위한 클러스터 노드이다.
  • 코드 라인 16~19에서 @fwspec의 두 번째 인자인 PPI hwirq에 대응하는 파티션 디스크립터와 클러스터 노드의 fwnode를 사용하여 hwirq를 알아온다.
  • 코드 라인 21~24에서 출력 인자 @hwirq에 알아온 hwirq를 대입하고, 출력 인자 @type에는 irq 타입 정보를 담아 정상 0 값을 반환한다.

 

partition_translate_id()

drivers/irqchip/irq-partition-percpu.c

int partition_translate_id(struct partition_desc *desc, void *partition_id)
{
        struct partition_affinity *part = NULL;
        int i;

        for (i = 0; i < desc->nr_parts; i++) {
                if (desc->parts[i].partition_id == partition_id) {
                        part = &desc->parts[i];
                        break;
                }
        }

        if (WARN_ON(!part)) {
                pr_err("Failed to find partition\n");
                return -EINVAL;
        }

        return i;
}

파티션 디스크립터와 @partition_id(fwnode)로 hwirq를 찾아온다. 찾지못하는 경우 nr_parts 값이 반환된다.

 

파티션용 irq 도메인 선택

gic_irq_domain_select()

drivers/irqchip/irq-gic-v3.c

static int gic_irq_domain_select(struct irq_domain *d,
                                 struct irq_fwspec *fwspec,
                                 enum irq_domain_bus_token bus_token)
{
        /* Not for us */
        if (fwspec->fwnode != d->fwnode)
                return 0;

        /* If this is not DT, then we have a single domain */
        if (!is_of_node(fwspec->fwnode))
                return 1;

        /*
         * If this is a PPI and we have a 4th (non-null) parameter,
         * then we need to match the partition domain.
         */
        if (fwspec->param_count >= 4 &&
            fwspec->param[0] == 1 && fwspec->param[3] != 0 &&
            gic_data.ppi_descs)
                return d == partition_get_domain(gic_data.ppi_descs[fwspec->param[1]]);

        return d == gic_data.domain;
}

@fwspec에 해당하는 irq 도메인을 선택한다. 입력 인자로 요청한 irq 도메인(@d)과 동일한 경우 1을 반환한다.

  • 코드 라인 6~7에서 인자로 받은 @fwspec->fwnode와 인자로 받은 irq 도메인의 fwnode가 서로 다른 경우 실패 값 0을 반환한다.
  • 코드 라인 10~11에서 디바이스 트리 노드가 아닌 경우 싱글 도메인만 있으므로 1을 반환한다.
  • 코드 라인 17~20에서 PPI이고 4개 이상의 파라미터를 가졌으며 파티션 도메인을 가진 경우 파티션 디스크립터에 등록한 irq 도메인과 인자로 받은 irq 도메인이 동일한지 여부를 반환한다.
    • 반드시 param_count가 4 이상이며 4번째 파라미터에는 클러스터 phandle 값이 있어야 한다.
  • 코드 라인 22에서 파티션 도메인이 아닌 일반 PPI/SPI의 경우 인자로 요청한 irq 도메인(@d)과 gic 기본 도메인이 동일하지 여부를 반환한다.

 

partition_get_domain()

drivers/irqchip/irq-partition-percpu.c

struct irq_domain *partition_get_domain(struct partition_desc *dsc)
{
        if (dsc)
                return dsc->domain;

        return NULL;
}

파티션 디스크립터에 소속된 irq 도메인을 반환한다.

 


구조체

partition_desc 구조체

drivers/irqchip/irq-partition-percpu.c

struct partition_desc {
        int                             nr_parts;
        struct partition_affinity       *parts;
        struct irq_domain               *domain;
        struct irq_desc                 *chained_desc;
        unsigned long                   *bitmap;
        struct irq_domain_ops           ops;
};

파티션 디스크립터이다.

  • nr_parts
    • 구성된 파티션 수이다.
    • 클러스터가 4개인 경우 nr_parts=4이다.
  • *parts
    • 파티션 수만큼 cpu 구성정보를 담는 partition_affinity 구조체들을 가리킨다.
  • *domain
    • 파티션에 소속된 파티션 도메인을 가리킨다.
  • *chained_desc
    • 상위 chained irq (PPI irq)를 가리킨다.
  • *bitmap
    • 운용되는 파티션에 대한 비트맵이다.
  • ops
    • 파티션에서 사용할 파티션 도메인용 operations이다.

 

partition_affinity 구조체

include/linux/irqchip/irq-partition-percpu.h

struct partition_affinity {
        cpumask_t                       mask;
        void                            *partition_id;
};

파티션이 사용할 cpu 구성정보를 담는다.

  •  mask
    • 파티션에 포함된 cpu 마스크
  • *partition_id
    • 디바이스 트리의 파티션 노드에 해당하는 fwnode_handle

 

참고

 

Interrupts -9- (GIC v3 Driver)

<kernel v5.4>

Interrupts -9- (GIC v3 Driver)

GIC v3 & GIC v4 호환

GIC v3 드라이버 코드는 GIC v4도 같이 사용한다. (GIC v4 its를 위한 코드가 일부 추가됨)

 

다음은 rock3399 칩에서 사용하는 gic v3 컨트롤러에 대한 디바이스 트리이다.

arch/arm64/boot/dts/rockchip/rk3399.dtsi

        gic: interrupt-controller@fee00000 {
                compatible = "arm,gic-v3";
                #interrupt-cells = <4>;
                #address-cells = <2>;
                #size-cells = <2>;
                ranges;
                interrupt-controller;

                reg = <0x0 0xfee00000 0 0x10000>, /* GICD */
                      <0x0 0xfef00000 0 0xc0000>, /* GICR */
                      <0x0 0xfff00000 0 0x10000>, /* GICC */
                      <0x0 0xfff10000 0 0x10000>, /* GICH */
                      <0x0 0xfff20000 0 0x10000>; /* GICV */
                interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH 0>;
                its: interrupt-controller@fee20000 {
                        compatible = "arm,gic-v3-its";
                        msi-controller;
                        reg = <0x0 0xfee20000 0x0 0x20000>;
                };

                ppi-partitions {
                        ppi_cluster0: interrupt-partition-0 {
                                affinity = <&cpu_l0 &cpu_l1 &cpu_l2 &cpu_l3>;
                        };

                        ppi_cluster1: interrupt-partition-1 {
                                affinity = <&cpu_b0 &cpu_b1>;
                        };
                };
        };
  • reg
    • 기본적으로 GICD(mandatory), GICR(mandatory), GICC(option), GICH(option), GICV(option) 영역 5개를 사용한다.
  • interrupts
    • VGIC 관리를 위한 인터럽트 소스이다.
  • its: interrupt-controller@fee20000
    • GIC v3-its 드라이버는 생략한다.
  • ppi-partitions
    • PPI affnity를 사용하여 파티션을 구성한다.

 

다음은 hisilicon의 hip07 칩에서 사용하는 gic v3 컨트롤러에 대한 디바이스 트리이다.

arch/arm64/boot/dts/hisilicon/hip07.dtsi

        gic: interrupt-controller@4d000000 {
                compatible = "arm,gic-v3";
                #interrupt-cells = <3>;
                #address-cells = <2>;
                #size-cells = <2>;
                ranges;
                interrupt-controller;
                #redistributor-regions = <4>;
                redistributor-stride = <0x0 0x40000>;
                reg = <0x0 0x4d000000 0x0 0x10000>,     /* GICD */
                      <0x0 0x4d100000 0x0 0x400000>,    /* p0 GICR node 0 */
                      <0x0 0x6d100000 0x0 0x400000>,    /* p0 GICR node 1 */
                      <0x400 0x4d100000 0x0 0x400000>,  /* p1 GICR node 2 */
                      <0x400 0x6d100000 0x0 0x400000>,  /* p1 GICR node 3 */
                      <0x0 0xfe000000 0x0 0x10000>,     /* GICC */
                      <0x0 0xfe010000 0x0 0x10000>,     /* GICH */
                      <0x0 0xfe020000 0x0 0x10000>;     /* GICV */
                interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH>;

                ...
  • #redistributor-regions
    • GICR 레지스터 영역이 4개의 노드로 구성됨을 알 수 있다.
  • redistributor-stride
    • GICR 노드 레지스터간에 패딩 간격 페이지 수를 지정한다. 이 값은 64K의 배수여야 한다.
      • 위의 예에서는 256K를 사용하였다.
  • reg
    • 기본적으로 GICD, GICR, GICC, GICH, GICV 영역 5개를 사용하지만, 이 컨트롤러는 GICR 영역이 1 -> 4개로 확장되었다.

 

GIC v3 드라이버 초기화

gic_of_init()

drivers/irqchip/irq-gic-v3.c

static int __init gic_of_init(struct device_node *node, struct device_node *parent)
{
        void __iomem *dist_base;
        struct redist_region *rdist_regs;
        u64 redist_stride;
        u32 nr_redist_regions;
        int err, i;

        dist_base = of_iomap(node, 0);
        if (!dist_base) {
                pr_err("%pOF: unable to map gic dist registers\n", node);
                return -ENXIO;
        }

        err = gic_validate_dist_version(dist_base);
        if (err) {
                pr_err("%pOF: no distributor detected, giving up\n", node);
                goto out_unmap_dist;
        }

        if (of_property_read_u32(node, "#redistributor-regions", &nr_redist_regions))
                nr_redist_regions = 1;

        rdist_regs = kcalloc(nr_redist_regions, sizeof(*rdist_regs),
                             GFP_KERNEL);
        if (!rdist_regs) {
                err = -ENOMEM;
                goto out_unmap_dist;
        }

        for (i = 0; i < nr_redist_regions; i++) {
                struct resource res;
                int ret;

                ret = of_address_to_resource(node, 1 + i, &res);
                rdist_regs[i].redist_base = of_iomap(node, 1 + i);
                if (ret || !rdist_regs[i].redist_base) {
                        pr_err("%pOF: couldn't map region %d\n", node, i);
                        err = -ENODEV;
                        goto out_unmap_rdist;
                }
                rdist_regs[i].phys_base = res.start;
        }

        if (of_property_read_u64(node, "redistributor-stride", &redist_stride))
                redist_stride = 0;

        gic_enable_of_quirks(node, gic_quirks, &gic_data);

        err = gic_init_bases(dist_base, rdist_regs, nr_redist_regions,
                             redist_stride, &node->fwnode);
        if (err)
                goto out_unmap_rdist;

        gic_populate_ppi_partitions(node);

        if (static_branch_likely(&supports_deactivate_key))
                gic_of_setup_kvm_info(node);
        return 0;

out_unmap_rdist:
        for (i = 0; i < nr_redist_regions; i++)
                if (rdist_regs[i].redist_base)
                        iounmap(rdist_regs[i].redist_base);
        kfree(rdist_regs);
out_unmap_dist:
        iounmap(dist_base);
        return err;
}
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init);

GIC v3 인터럽트 컨트롤러 초기화 시작 함수이다. (GIC v4 인터럽트 컨트롤러도 이 함수를 같이 사용한다)

  • 코드 라인 9~13에서 Distributor 레지스터 영역인 “reg” 속성의 0번째 주소와 사이즈를 읽어와 가상 주소에 매핑한다.
  • 코드 라인 15~19에서 Distributor 레지스터에서 gic v3 또는 v4 여부를 체크한다. (성공 시 0)
  • 코드 라인 21~22에서 ReDistributor 영역의 수를 알아오기 위해 “#redistributor-regions” 속성 값을 알아온다. 속성이 없는 경우 디폴트는 1이다.
  • 코드 라인 24~29에서 ReDistributor 영역의 수만큼 redist_region 구조체를 할당한다.
  • 코드 라인 31~43에서 ReDistributor 영역의 수만큼 “reg” 속성의 두 번째 부터 주소와 사이즈를 읽어 가상 주소에 매핑하고, 매핑한 주소는 rdist_regs[].redist_base에 대입한다. 그리고 rdist_regs[].phys_base에는 물리 주소를 기록한다.
  • 코드 라인 45~46에서 GICR 노드 레지스터간에 패딩 간격 페이지 수를 지정하기 위해 “redistributor-stride” 속성 값을 알아온다. 속성이 없는 경우 디폴트는 0이다.
  • 코드 라인 48에서 gic 컨트롤러들 중 errata 코드가 있는 경우 이를 호출하여 수행한다.
    • 현재 Qualcomm MSM8996, Hisilicon의 hip06 및 hip07이 대상이다.
  • 코드 라인 50~53에서 gic 컨트롤러를 초기화한다.
  • 코드 라인 55에서 모든 PPI들을 위해 irq 파티션을 생성한다.
  • 코드 라인 57~58에서 Guest OS를 위한 KVM 정보를 설정한다.
  • 코드 라인 59에서 성공 값 0을 반환한다.

 

gic_validate_dist_version()

drivers/irqchip/irq-gic-v3.c

static int __init gic_validate_dist_version(void __iomem *dist_base)
{
        u32 reg = readl_relaxed(dist_base + GICD_PIDR2) & GIC_PIDR2_ARCH_MASK;

        if (reg != GIC_PIDR2_ARCH_GICv3 && reg != GIC_PIDR2_ARCH_GICv4)
                return -ENODEV;

        return 0;
}

GICD_PIDR2 레지스터의 버전을 읽어 gic v3 또는 v4인지 여부를 알아온다. 성공시 0을 반환한다.

 

GIC 레지스터들 초기화

gic_init_bases()

drivers/irqchip/irq-gic-v3.c

static int __init gic_init_bases(void __iomem *dist_base,
                                 struct redist_region *rdist_regs,
                                 u32 nr_redist_regions,
                                 u64 redist_stride,
                                 struct fwnode_handle *handle)
{
        u32 typer;
        int err;

        if (!is_hyp_mode_available())
                static_branch_disable(&supports_deactivate_key);

        if (static_branch_likely(&supports_deactivate_key))
                pr_info("GIC: Using split EOI/Deactivate mode\n");

        gic_data.fwnode = handle;
        gic_data.dist_base = dist_base;
        gic_data.redist_regions = rdist_regs;
        gic_data.nr_redist_regions = nr_redist_regions;
        gic_data.redist_stride = redist_stride;

        /*
         * Find out how many interrupts are supported.
         */
        typer = readl_relaxed(gic_data.dist_base + GICD_TYPER);
        gic_data.rdists.gicd_typer = typer;

        gic_enable_quirks(readl_relaxed(gic_data.dist_base + GICD_IIDR),
                          gic_quirks, &gic_data);

        pr_info("%d SPIs implemented\n", GIC_LINE_NR - 32);
        pr_info("%d Extended SPIs implemented\n", GIC_ESPI_NR);
        gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops,
                                                 &gic_data);
        irq_domain_update_bus_token(gic_data.domain, DOMAIN_BUS_WIRED);
        gic_data.rdists.rdist = alloc_percpu(typeof(*gic_data.rdists.rdist));
        gic_data.rdists.has_vlpis = true;
        gic_data.rdists.has_direct_lpi = true;

        if (WARN_ON(!gic_data.domain) || WARN_ON(!gic_data.rdists.rdist)) {
                err = -ENOMEM;
                goto out_free;
        }

        gic_data.has_rss = !!(typer & GICD_TYPER_RSS);
        pr_info("Distributor has %sRange Selector support\n",
                gic_data.has_rss ? "" : "no ");

        if (typer & GICD_TYPER_MBIS) {
                err = mbi_init(handle, gic_data.domain);
                if (err)
                        pr_err("Failed to initialize MBIs\n");
        }

        set_handle_irq(gic_handle_irq);

        gic_update_rdist_properties();

        gic_smp_init();
        gic_dist_init();
        gic_cpu_init();
        gic_cpu_pm_init();

        if (gic_dist_supports_lpis()) {
                its_init(handle, &gic_data.rdists, gic_data.domain);
                its_cpu_init();
        } else {
                if (IS_ENABLED(CONFIG_ARM_GIC_V2M))
                        gicv2m_init(handle, gic_data.domain);
        }

        gic_enable_nmi_support();

        return 0;

out_free:
        if (gic_data.domain)
                irq_domain_remove(gic_data.domain);
        free_percpu(gic_data.rdists.rdist);
        return err;
}
  • 코드 라인 10~14에서 하이퍼 모드가 운용가능한 상태가 아니면 supports_deactivate_key를 disable 한다.
    • 커널이 하이퍼 모드로  동작하는 경우에 따라 EOI 모드를 priority drop과 deactivate를 split하여 운영한다.
      • 예) “GIC: Using split EOI/Deactivate mode”
    • 참고: 가상화 지원 (하이퍼 모드) | 문c
  • 코드 라인 16~20에서 gic_chip_data 구조체 형태인 전역 gic_data에 인자로 전달된 값들로 초기화한다.
  • 코드 라인 25~26에서 GICD_TYPER 레지스터 값을 저장한다.
  • 코드 라인 28~29에서 GICD_IIDR 레지스터 값을 읽어 관련 디바이스에 대해서만 errtum 코드를 수행한다.
  • 코드 라인 31~32에서 SPI와 확장 SPI 수 정보를 출력한다.
  • 코드 라인 33~35에서 하이라키를 지원하는 irq 도메인을 생성하고, DOMAIN_BUS_WIRED 도메인 타입으로 지정한다.
  • 코드 라인 36~43에서 Redistributor용 per-cpu 자료 구조를 할당 받고 초기화한다.
    • 가상 LPI 지원과 Direct LPI Injection을 true로 미리 설정한다.
    • 추후 Redistributor 레지스터들을 읽어 하나라도 VLPI 및 DirectLPI 값이 0인 경우 false로 바뀐다.
  • 코드 라인 45~47에서 GICD_TYPER.RSS 비트를 읽어 rss 지원 여부를 알아내고 출력한다.
  • 코드 라인 49~53에서 GICD_TYPER.MBIS 비트를 읽어 메시지 기반 인터럽트의 지원하는 경우 mbi 관련 초기화 루틴을 수행한다.
    • “msi-controller” 속성 필요
  • 코드 라인 55에서 이 컨트롤러에서 관리를 위해 사용하는 인터럽트의 핸들러 함수를 지정한다.
  • 코드 라인 57에서 Redistributor 속성을 읽어 갱신한다.
  • 코드 라인 59에서 SMP를 지원하기 위한 초기화를 수행한다.
  • 코드 라인 60에서 Distributtor 레지스터를 사용한 초기화를 수행한다.
  • 코드 라인 61에서 CPU interface 레지스터를 사용한 초기화를 수행한다.
  • 코드 라인 62에서 CPU 절전(suspen/resume) 관련한 초기화를 수행한다.
  • 코드 라인 64~70에서 LPI를 지원하는 경우 ITS츨 초기화한다. 지원하지 않는 경우 GIC 가상화 지원 초기화를 수행한다.
  • 코드 라인 72에서 Pesudo-NNI 기능을 지원하는 경우 이에 대한 초기화를 수행한다.
  • 코드 라인 74에서 성공 값 0을 반환한다.

 

GIC Quirks(ERRATA) 초기화 및 적용

gic_enable_of_quirks()

drivers/irqchip/irq-gic-common.c

void gic_enable_of_quirks(const struct device_node *np,
                          const struct gic_quirk *quirks, void *data)
{
        for (; quirks->desc; quirks++) {
                if (!of_device_is_compatible(np, quirks->compatible))
                        continue;
                if (quirks->init(data))
                        pr_info("GIC: enabling workaround for %s\n",
                                quirks->desc);
        }
}

디바이스 트리로 지정된 디바이스만 erratum 적용한다.

 

gic_enable_quirks()

drivers/irqchip/irq-gic-common.c

void gic_enable_quirks(u32 iidr, const struct gic_quirk *quirks,
                void *data)
{
        for (; quirks->desc; quirks++) {
                if (quirks->compatible)
                        continue;
                if (quirks->iidr != (quirks->mask & iidr))
                        continue;
                if (quirks->init(data))
                        pr_info("GIC: enabling workaround for %s\n",
                                quirks->desc);
        }
}

@iidr 값이 일치하는 디바이스만 erratum 적용한다.

 

gic_quirks

static const struct gic_quirk gic_quirks[] = {
        {
                .desc   = "GICv3: Qualcomm MSM8996 broken firmware",
                .compatible = "qcom,msm8996-gic-v3",
                .init   = gic_enable_quirk_msm8996,
        },
        {
                .desc   = "GICv3: HIP06 erratum 161010803",
                .iidr   = 0x0204043b,
                .mask   = 0xffffffff,
                .init   = gic_enable_quirk_hip06_07,
        },
        {
                .desc   = "GICv3: HIP07 erratum 161010803",
                .iidr   = 0x00000000,
                .mask   = 0xffffffff,
                .init   = gic_enable_quirk_hip06_07,
        },
        {
        }
};

버그 등으로 인해 erratum 코드가 적용되어야 할 디바이스들이다.

 

SMP core들 시작시 호출될 함수 지정

gic_smp_init()

drivers/irqchip/irq-gic-v3.c

static void gic_smp_init(void)
{
        set_smp_cross_call(gic_raise_softirq);
        cpuhp_setup_state_nocalls(CPUHP_AP_IRQ_GIC_STARTING,
                                  "irqchip/arm/gicv3:starting",
                                  gic_starting_cpu, NULL);
}

새로운 cpu가 online될 때 gic에 알려야 할 함수를 지정한다.

 

gic_starting_cpu()

drivers/irqchip/irq-gic-v3.c

static int gic_starting_cpu(unsigned int cpu)
{
        gic_cpu_init();

        if (gic_dist_supports_lpis())
                its_cpu_init();

        return 0;
}

새로운 cpu가 online될 때 콜백 호출되는 함수이다. 해당 cpu interface 레지스터들을 초기화하고, LPI를 지원하는 경우 ITS 초기화 함수를 수행한다.

 


Distributor 초기화

gic_dist_init()

drivers/irqchip/irq-gic-v3.c

static void __init gic_dist_init(void)
{
        unsigned int i;
        u64 affinity;
        void __iomem *base = gic_data.dist_base;

        /* Disable the distributor */
        writel_relaxed(0, base + GICD_CTLR);
        gic_dist_wait_for_rwp();

        /*
         * Configure SPIs as non-secure Group-1. This will only matter
         * if the GIC only has a single security state. This will not
         * do the right thing if the kernel is running in secure mode,
         * but that's not the intended use case anyway.
         */
        for (i = 32; i < GIC_LINE_NR; i += 32)
                writel_relaxed(~0, base + GICD_IGROUPR + i / 8);

        /* Extended SPI range, not handled by the GICv2/GICv3 common code */
        for (i = 0; i < GIC_ESPI_NR; i += 32) {
                writel_relaxed(~0U, base + GICD_ICENABLERnE + i / 8);
                writel_relaxed(~0U, base + GICD_ICACTIVERnE + i / 8);
        }

        for (i = 0; i < GIC_ESPI_NR; i += 32)
                writel_relaxed(~0U, base + GICD_IGROUPRnE + i / 8);

        for (i = 0; i < GIC_ESPI_NR; i += 16)
                writel_relaxed(0, base + GICD_ICFGRnE + i / 4);

        for (i = 0; i < GIC_ESPI_NR; i += 4)
                writel_relaxed(GICD_INT_DEF_PRI_X4, base + GICD_IPRIORITYRnE + i);

        /* Now do the common stuff, and wait for the distributor to drain */
        gic_dist_config(base, GIC_LINE_NR, gic_dist_wait_for_rwp);

        /* Enable distributor with ARE, Group1 */
        writel_relaxed(GICD_CTLR_ARE_NS | GICD_CTLR_ENABLE_G1A | GICD_CTLR_ENABLE_G1,
                       base + GICD_CTLR);

        /*
         * Set all global interrupts to the boot CPU only. ARE must be
         * enabled.
         */
        affinity = gic_mpidr_to_affinity(cpu_logical_map(smp_processor_id()));
        for (i = 32; i < GIC_LINE_NR; i++)
                gic_write_irouter(affinity, base + GICD_IROUTER + i * 8);

        for (i = 0; i < GIC_ESPI_NR; i++)
                gic_write_irouter(affinity, base + GICD_IROUTERnE + i * 8);
}

GIC Distributor를 초기화한다.

  • 코드 라인 8~9에서 Distributor를 disable하기 위해 GICD_CTRL 값에 0을 기록한다.
  • 코드 라인 17~18에서 모든 SPI들을 non-secure group 1으로 설정하기위해 GICD_IGROUPRn들을 모두 0xffff_ffff(1 비트당 1 인터럽트)로 설정한다.
    • one security states에서 리눅스 커널이 동작하는 경우 그룹 제어가 가능하다.
    • 그러나 two secure states인 경우 그룹 설정은 secure에서 해야 하기 때문에 non-secure에서 동작하느 리눅스 커널에서 제어는 의미 없게 된다.
  • 코드 라인 21~24에서 모든 확장 SPI들을 disable 및 deactivate 요청하기 위해 GICD_ICENABLERn 과 GICD_ICACTIVERn들을 모두 0xffff_ffff(1 비트당 1 인터럽트)로 설정한다.
  • 코드 라인 26~27에서 모든 확장 SPI들을 non-secure group 1으로 설정하기위해 GICD_IGROUPRnE들을 모두 0xffff_ffff(1 비트당 1 인터럽트)로 설정한다.
  • 코드 라인 29~30에서  모든 확장 SPI들을 레벨 sensitive 모드로 설정하기 위해 GICD_ICFGRnE들을 모두 0x0(1 비트당 1 인터럽트)으로 설정한다.
  • 코드 라인 32~33에서 모든 확장 SPI들에 대해 우선 순위를 0xd0으로 설정하기 위해 GICD_IPRIORITYRnE에 0xa0을 기록한다. (8 비트당 1 인터럽트)
    • non-secure에서 설정한 0xa0은 cpu interface로 올라갈 때 내부 변환하면 0xa0 >> 1 | 0x80 = 0xd0에 해당하는 우선 순위이다.
  • 코드 라인 36에서 GIC v1 이상에서 동작하는 공통 Distributor 설정 코드를 수행한다.
  • 코드 라인 39~40에서 Distributor를 활성화할 때 ARE(Affinity Routing Enable), group 1도 활성화한다.
  • 코드 라인 46에서 boot cpu의 affinity 값들을 읽어오기 위해 MPIDR 레지스터를 알아온다.
  • 코드 라인 47~51에서 모든 SPI들 및 확장 SPI들을 대상으로 boot cpu로 라우팅 설정하기 위해 읽어온 affinity 값을 GICD_ROUTERn 및 GICD_ROUTERRnE에 기록한다.

 

gic_dist_wait_for_rwp()

drivers/irqchip/irq-gic-v3.c

/* Wait for completion of a distributor change */
static void gic_dist_wait_for_rwp(void)
{
        gic_do_wait_for_rwp(gic_data.dist_base);
}

distributor 레지스터에 기록한 후 이의 수행이 완료될 때까지 대기한다.

 

gic_do_wait_for_rwp()

drivers/irqchip/irq-gic-v3.c

static void gic_do_wait_for_rwp(void __iomem *base)
{
        u32 count = 1000000;    /* 1s! */

        while (readl_relaxed(base + GICD_CTLR) & GICD_CTLR_RWP) {
                count--;
                if (!count) {
                        pr_err_ratelimited("RWP timeout, gone fishing\n");
                        return;
                }
                cpu_relax();
                udelay(1);
        };
}

GICD_CTLR.RWP 레지스터를 읽어 0이될 때까지 반복한다.

 

GIC v1 이상 공통 Distributor 설정

gic_dist_config()

drivers/irqchip/irq-gic-v3.c

void gic_dist_config(void __iomem *base, int gic_irqs,
                     void (*sync_access)(void))
{
        unsigned int i;

        /*
         * Set all global interrupts to be level triggered, active low.
         */
        for (i = 32; i < gic_irqs; i += 16)
                writel_relaxed(GICD_INT_ACTLOW_LVLTRIG,
                                        base + GIC_DIST_CONFIG + i / 4);

        /*
         * Set priority on all global interrupts.
         */
        for (i = 32; i < gic_irqs; i += 4)
                writel_relaxed(GICD_INT_DEF_PRI_X4, base + GIC_DIST_PRI + i);

        /*
         * Deactivate and disable all SPIs. Leave the PPI and SGIs
         * alone as they are in the redistributor registers on GICv3.
         */
        for (i = 32; i < gic_irqs; i += 32) {
                writel_relaxed(GICD_INT_EN_CLR_X32,
                               base + GIC_DIST_ACTIVE_CLEAR + i / 8);
                writel_relaxed(GICD_INT_EN_CLR_X32,
                               base + GIC_DIST_ENABLE_CLEAR + i / 8);
        }

        if (sync_access)
                sync_access();
}

모든 GIC 버전에서 동작하는 공통 Distributor 설정 코드를 수행한다.

  • 코드 라인 9~11에서 모든 SPI들에 대한 low active 레벨 sensitive 설정을 위해 GICD_ICFGRn에 0x0을 기록한다. (2 비트당 1 인터럽트)
  • 코드 라인 16~17에서 모든 SPI들에 대해 우선 순위를 0xd0으로 설정하기 위해 GICD_IPRIORITYRn에 0xa0을 기록한다. (8 비트당 1 인터럽트)
    • non-secure에서 설정한 0xa0은 cpu interface로 올라갈 때 내부 변환하면 0xa0 >> 1 | 0x80 = 0xd0에 해당하는 우선 순위이다.
  • 코드 라인 23~28에서 모든 확장 SPI들을 disable 및 deactivate 요청하기 위해 GICD_ICENABLERn 과 GICD_ICACTIVERn들을 모두 0xffff_ffff(1 비트당 1 인터럽트)로 설정한다.
  • 코드 라인 30~31 Distributor 레지스터 적용이 완료될 때까지 대기하도록 인자로 전달받은 @sync_access 함수를 호출한다.

 


CPU Interface 관련 초기화

gic_cpu_init()

drivers/irqchip/irq-gic-v3.c

static void gic_cpu_init(void)
{
        void __iomem *rbase;
        int i;

        /* Register ourselves with the rest of the world */
        if (gic_populate_rdist())
                return;

        gic_enable_redist(true);

        WARN((gic_data.ppi_nr > 16 || GIC_ESPI_NR != 0) &&
             !(gic_read_ctlr() & ICC_CTLR_EL1_ExtRange),
             "Distributor has extended ranges, but CPU%d doesn't\n",
             smp_processor_id());

        rbase = gic_data_rdist_sgi_base();

        /* Configure SGIs/PPIs as non-secure Group-1 */
        for (i = 0; i < gic_data.ppi_nr + 16; i += 32)
                writel_relaxed(~0, rbase + GICR_IGROUPR0 + i / 8);

        gic_cpu_config(rbase, gic_data.ppi_nr + 16, gic_redist_wait_for_rwp);

        /* initialise system registers */
        gic_cpu_sys_reg_init();
}

CPU interface를 초기화한다.

  • 코드 라인 7~10에서 Redistributor를 활성화한다.
  • 코드 라인 17에서 Redistributor에서 SGI 베이스 레지스터 가상 주소를 알아온다.
  • 코드 라인 20~21에서 SGI/PPI들을 모두 non-secure group 1으로 설정하기 위해 GICR_IGROUP0Rn에 0xffff_ffff(1 비트당 1 인터럽트)를 기록한다.
  • 코드 라인 23에서 모든 버전의 GIC를 위한 공통 CPU 설정 코드를 수행한다.
  • 코드 라인 26에서 시스템 레지스터를 초기화한다.

 

GIC v1 이상 공통 CPU 관련 설정

gic_cpu_config()

drivers/irqchip/irq-gic-common.c

void gic_cpu_config(void __iomem *base, int nr, void (*sync_access)(void))
{
        int i;

        /*
         * Deal with the banked PPI and SGI interrupts - disable all
         * private interrupts. Make sure everything is deactivated.
         */
        for (i = 0; i < nr; i += 32) {
                writel_relaxed(GICD_INT_EN_CLR_X32,
                               base + GIC_DIST_ACTIVE_CLEAR + i / 8);
                writel_relaxed(GICD_INT_EN_CLR_X32,
                               base + GIC_DIST_ENABLE_CLEAR + i / 8);
        }

        /*
         * Set priority on PPI and SGI interrupts
         */
        for (i = 0; i < nr; i += 4)
                writel_relaxed(GICD_INT_DEF_PRI_X4,
                                        base + GIC_DIST_PRI + i * 4 / 4);

        /* Ensure all SGI interrupts are now enabled */
        writel_relaxed(GICD_INT_EN_SET_SGI, base + GIC_DIST_ENABLE_SET);

        if (sync_access)
                sync_access();
}

모든 GIC 버전에서 동작하는 공통 CPU 관련 설정을 수행한다.

  • 코드 라인 9~14에서 모든 SGI/PPI들을 disable 및 deactivate 요청하기 위해 GICD_ICENABLERn 과 GICD_ICACTIVERn들을 모두 0xffff_ffff(1 비트당 1 인터럽트)로 설정한다.
  • 코드 라인 19~21에서 모든 SGI/PPI들에 대해 우선 순위를 0xd0으로 설정하기 위해 GICD_IPRIORITYRn에 0xa0을 기록한다. (8 비트당 1 인터럽트)
    • non-secure에서 설정한 0xa0은 cpu interface로 올라갈 때 내부 변환하면 0xa0 >> 1 | 0x80 = 0xd0에 해당하는 우선 순위이다.
  • 코드 라인 24에서 모든 SGI들을 enable 요청하기 위해 GICD_ISENABLERn들을 모두 0xffff_ffff(1 비트당 1 인터럽트)로 설정한다.
  • 코드 라인 26~27에서 Distributor 레지스터 적용이 완료될 때까지 대기하도록 인자로 전달받은 @sync_access 함수를 호출한다.

 

gic_cpu_sys_reg_init()

drivers/irqchip/irq-gic-v3.c -1/2-

static void gic_cpu_sys_reg_init(void)
{
        int i, cpu = smp_processor_id();
        u64 mpidr = cpu_logical_map(cpu);
        u64 need_rss = MPIDR_RS(mpidr);
        bool group0;
        u32 pribits;

        /*
         * Need to check that the SRE bit has actually been set. If
         * not, it means that SRE is disabled at EL2. We're going to
         * die painfully, and there is nothing we can do about it.
         *
         * Kindly inform the luser.
         */
        if (!gic_enable_sre())
                pr_err("GIC: unable to set SRE (disabled at EL2), panic ahead\n");

        pribits = gic_get_pribits();

        group0 = gic_has_group0();

        /* Set priority mask register */
        if (!gic_prio_masking_enabled()) {
                write_gicreg(DEFAULT_PMR_VALUE, ICC_PMR_EL1);
        } else {
                /*
                 * Mismatch configuration with boot CPU, the system is likely
                 * to die as interrupt masking will not work properly on all
                 * CPUs
                 */
                WARN_ON(gic_supports_nmi() && group0 &&
                        !gic_dist_security_disabled());
        }

        /*
         * Some firmwares hand over to the kernel with the BPR changed from
         * its reset value (and with a value large enough to prevent
         * any pre-emptive interrupts from working at all). Writing a zero
         * to BPR restores is reset value.
         */
        gic_write_bpr1(0);

        if (static_branch_likely(&supports_deactivate_key)) {
                /* EOI drops priority only (mode 1) */
                gic_write_ctlr(ICC_CTLR_EL1_EOImode_drop);
        } else {
                /* EOI deactivates interrupt too (mode 0) */
                gic_write_ctlr(ICC_CTLR_EL1_EOImode_drop_dir);
        }

GIC 시스템 레지스터들을 초기화한다.

  • 코드 라인 3~5에서 현재 cpu에 대한 mpidr 값을 알아와서 aff0 값이 16을 초과한 경우 need_rss에 1을 대입한다.
    • aff0이 16 이상이라는 의미는 최하위 레벨에서 16개 이상의 cpu가 사용되므로 RSS(Range Select Support)=1이되어 최하위 affnity 레벨의 cpu 범위가 0~255까지 라는 의미이다.
  • 코드 라인 16~17에서 SRE(System Register Enable)이 0인 경우 에러를 출력한다.
    • GICv3 하드웨어가 GIC v1~v2 드라이버 코드를 사용하는 경우 SRE=0로 설정하지만, GIC v3 하드웨어에 GIC v3 드라이버 코드를 사용하는 경우 SRE=1로 하여 시스템 레지스터를 사용하는 것이 정확하고, 또 성능에도 유리하다.
  • 코드 라인 19에서 pribits 값을 알아온다.
    • ICC_CTLR_EL1.PRIbits + 1 값
  • 코드 라인 21에서 커널이 group 0를 사용 가능한지 여부를 알아온다.
  • 코드 라인 24~34에서 Pesudo-NMI 기능을 위한 priority masking 기능이 활성화되지 않은 경우 디폴트 priority mask 값 0xf0을 ICC_PMR_EL1.PMR 레지스터에 기록한다.
  • 코드 라인 42에서 펌웨어에서 Binary Point 값을 설정하였을 수도 있으므로 커널에선 이를 0으로 기록하여 리셋되는 기능이 있다.
  • 코드 라인 44~50에서 EL2 부트 여부에 따라 적절한 EOImode를 설정한다.
    • EL2 모드에서 리눅스 커널이 부팅한 경우 호스트 OS로 동작하는 경우이다. 이러한 경우 Guest OS를 지원하기 위해 Host OS는 priority drop과 deactivate를 분리한(ICC_CTLR_EL1_EOImode_drop) EOI 모드를 사용한다.

 

drivers/irqchip/irq-gic-v3.c -2/2-

        /* Always whack Group0 before Group1 */
        if (group0) {
                switch(pribits) {
                case 8:
                case 7:
                        write_gicreg(0, ICC_AP0R3_EL1);
                        write_gicreg(0, ICC_AP0R2_EL1);
                /* Fall through */
                case 6:
                        write_gicreg(0, ICC_AP0R1_EL1);
                /* Fall through */
                case 5:
                case 4:
                        write_gicreg(0, ICC_AP0R0_EL1);
                }

                isb();
        }

        switch(pribits) {
        case 8:
        case 7:
                write_gicreg(0, ICC_AP1R3_EL1);
                write_gicreg(0, ICC_AP1R2_EL1);
                /* Fall through */
        case 6:
                write_gicreg(0, ICC_AP1R1_EL1);
                /* Fall through */
        case 5:
        case 4:
                write_gicreg(0, ICC_AP1R0_EL1);
        }

        isb();

        /* ... and let's hit the road... */
        gic_write_grpen1(1);

        /* Keep the RSS capability status in per_cpu variable */
        per_cpu(has_rss, cpu) = !!(gic_read_ctlr() & ICC_CTLR_EL1_RSS);

        /* Check all the CPUs have capable of sending SGIs to other CPUs */
        for_each_online_cpu(i) {
                bool have_rss = per_cpu(has_rss, i) && per_cpu(has_rss, cpu);

                need_rss |= MPIDR_RS(cpu_logical_map(i));
                if (need_rss && (!have_rss))
                        pr_crit("CPU%d (%lx) can't SGI CPU%d (%lx), no RSS\n",
                                cpu, (unsigned long)mpidr,
                                i, (unsigned long)cpu_logical_map(i));
        }

        /**
         * GIC spec says, when ICC_CTLR_EL1.RSS==1 and GICD_TYPER.RSS==0,
         * writing ICC_ASGI1R_EL1 register with RS != 0 is a CONSTRAINED
         * UNPREDICTABLE choice of :
         *   - The write is ignored.
         *   - The RS field is treated as 0.
         */
        if (need_rss && (!gic_data.has_rss))
                pr_crit_once("RSS is required but GICD doesn't support it\n");
}
  • 코드 라인 2~18에서 커널에서 group 0을 사용가능한 경우 pribits에 따라4 개의 Activate Priorities Group 0 레지스터값에 0을 기록하여 리셋하여 사용한다.
    • ICC_AP0Rn_EL1 레지스터 4개는 EL2에서 physical fiq를 수신하지 않도록 설정된 경우(EL2.HCR_EL2.FMO=0)에만 non-secure EL1 커널에서 access 가능하다.
  • 코드 라인 20~34에서 pribits에 따라4 개의 Activate Priorities Group 1 레지스터값에 0을 기록하여 리셋하여 사용한다.
    • ICC_AP1Rn_EL1 레지스터 4개는 EL2에서 physical irq를 수신하지 않도록 설정된 경우(EL2.HCR_EL2.IMO=0)에만 non-secure EL1 커널에서 access 가능하다.
  • 코드 라인 37에서 non-secure 커널에서 group1 인터럽트를 활성화하기위해 SYS_ICC_IGRPEN1_EL1에 1을 기록한다.
  • 코드 라인 40에서 per-cpu has_rss 변수에 RSS(Range Select Support) 값을 알아온다.
    • SGI 사용시 0인 경우 0~15 cpu, 1인 경우 0~255 cpu
  • 코드 라인 43~51에서 cpu mpidr 값에서 affinity 0에 해당하는 cpu 번호가 RSS 범위를 벗어나는 경우 이를 체크하여 에러 로그를 출력한다.
  • 코드 라인 60~61에서 affinity 0 레벨에서 0~255 cpu 범위를 사용하는데 GIC가 지원하지 않는 경우 에러 로그를 출력한다.

 

gic_enable_sre()

include/linux/irqchip/arm-gic-v3.h

static inline bool gic_enable_sre(void)
{
        u32 val;

        val = gic_read_sre();
        if (val & ICC_SRE_EL1_SRE)
                return true;

        val |= ICC_SRE_EL1_SRE;
        gic_write_sre(val);
        val = gic_read_sre();

        return !!(val & ICC_SRE_EL1_SRE);
}

GIC 시스템 레지스터를 사용할 수 있도록 활성화한다.

  • ICC_SRE_EL1.SRE=1로 활성화한다.

 

gic_get_pribits()

drivers/irqchip/irq-gic-v3.c

static u32 gic_get_pribits(void)
{
        u32 pribits;

        pribits = gic_read_ctlr();
        pribits &= ICC_CTLR_EL1_PRI_BITS_MASK;
        pribits >>= ICC_CTLR_EL1_PRI_BITS_SHIFT;
        pribits++;

        return pribits;
}

priority 레벨링의 단계에 사용되는 비트 수를 알아온다.

  • ICC_CTRL_EL1.PRIbits + 1
    • 이 값이 4인 경우 -> pribits=5가 되고 2^5=32가 된다. (이 칩은 총 32 단계의 priority 레벨을 운영한다.)

 

gic_has_group0()

drivers/irqchip/irq-gic-v3.c

static bool gic_has_group0(void)
{
        u32 val;
        u32 old_pmr;

        old_pmr = gic_read_pmr();

        /*
         * Let's find out if Group0 is under control of EL3 or not by
         * setting the highest possible, non-zero priority in PMR.
         *
         * If SCR_EL3.FIQ is set, the priority gets shifted down in
         * order for the CPU interface to set bit 7, and keep the
         * actual priority in the non-secure range. In the process, it
         * looses the least significant bit and the actual priority
         * becomes 0x80. Reading it back returns 0, indicating that
         * we're don't have access to Group0.
         */
        gic_write_pmr(BIT(8 - gic_get_pribits()));
        val = gic_read_pmr();

        gic_write_pmr(old_pmr);

        return val != 0;
}

EL1에서 동작하는 리눅스 커널에서 group 0를 사용할 수 있는지 여부를 알아온다.

  • group 0가 EL3에서 통제되는 경우 EL1 에서 동작하는 커널에서 최고 높은 우선 순위를 기록하고 읽을 때 그 값이 읽히는 것이 아니라 0이 읽힌다.
  • pribits가 5인 경우 32단계로 priority 레벨링이 동작한다. 그 중 0x00을 제외한 가장 높은 우선 순위인 0x08을 기록해본다.
    • 0x00, 0x08, 0x10, 0x18, 0x20, … 0xf8

 

gic_prio_masking_enabled()

arch/arm64/include/asm/arch_gicv3.h

static inline bool gic_prio_masking_enabled(void)
{
        return system_uses_irq_prio_masking();
}

Pesudo-NMI 기능을 위해 시스템이 irq masking 기능을 사용할 수 있는 경우 true를 반환한다.

 

system_uses_irq_prio_masking()

arch/arm64/include/asm/cpufeature.h

static inline bool system_uses_irq_prio_masking(void)
{
        return IS_ENABLED(CONFIG_ARM64_PSEUDO_NMI) &&
               cpus_have_const_cap(ARM64_HAS_IRQ_PRIO_MASKING);
}

CONFIG_ARM64_PSEUDO_NMI 커널 옵션과 IRQ_PRIO_MASKING capability를 가진 경우에 true를 반환한다.

 


Redistributor 초기화

Redistributor 속성들 갱신

gic_update_rdist_properties()

drivers/irqchip/irq-gic-v3.c

static void gic_update_rdist_properties(void)
{
        gic_data.ppi_nr = UINT_MAX;
        gic_iterate_rdists(__gic_update_rdist_properties);
        if (WARN_ON(gic_data.ppi_nr == UINT_MAX))
                gic_data.ppi_nr = 0;
        pr_info("%d PPIs implemented\n", gic_data.ppi_nr);
        pr_info("%sVLPI support, %sdirect LPI support\n",
                !gic_data.rdists.has_vlpis ? "no " : "",
                !gic_data.rdists.has_direct_lpi ? "no " : "");
}

Redistributor들에서 몇 가지 속성을 갱신한다. 그런 후 관련 속성을 정보 출력한다.

 

gic_iterate_rdists()

drivers/irqchip/irq-gic-v3.c

static int gic_iterate_rdists(int (*fn)(struct redist_region *, void __iomem *))
{
        int ret = -ENODEV;
        int i;

        for (i = 0; i < gic_data.nr_redist_regions; i++) {
                void __iomem *ptr = gic_data.redist_regions[i].redist_base;
                u64 typer;
                u32 reg;

                reg = readl_relaxed(ptr + GICR_PIDR2) & GIC_PIDR2_ARCH_MASK;
                if (reg != GIC_PIDR2_ARCH_GICv3 &&
                    reg != GIC_PIDR2_ARCH_GICv4) { /* We're in trouble... */
                        pr_warn("No redistributor present @%p\n", ptr);
                        break;
                }

                do {
                        typer = gic_read_typer(ptr + GICR_TYPER);
                        ret = fn(gic_data.redist_regions + i, ptr);
                        if (!ret)
                                return 0;

                        if (gic_data.redist_regions[i].single_redist)
                                break;

                        if (gic_data.redist_stride) {
                                ptr += gic_data.redist_stride;
                        } else {
                                ptr += SZ_64K * 2; /* Skip RD_base + SGI_base */
                                if (typer & GICR_TYPER_VLPIS)
                                        ptr += SZ_64K * 2; /* Skip VLPI_base + reserved page */
                        }
                } while (!(typer & GICR_TYPER_LAST));
        }

        return ret ? -ENODEV : 0;
}

Redistributor들에서 몇 가지 속성을 갱신한다.

  • 코드 라인 6~16에서 Redistributor 영역 수 만큼 순회하며 GICR_PIDR2.ARCH 값을 읽어 GIC v3 및 v4가 아닌 경우 경고 메시지를 출력하고 에러를 반환한다.
  • 코드 라인 18~22에서 Redistributor 레지스터를 읽어 gic 설정 값을 갱신하기 위해 인자로 전달받은 *fn함수를 호출한다.
    • GIC v3에서 전달한 함수: __gic_update_rdist_properties()
  • 코드 라인 24~25에서 ACPI를 통해 하나의 Residtributor만 구현되었다고 지정된 경우 처리를 중간하고 함수를 빠져나간다.
  • 코드 라인 27~33에서 Resistributor 레지스터들 간에 redist_stride 값 만큼 갭을 둔다. 단 redist_stride가 설정되지 않은 경우 128K/256K(LPIS 지원시) 만큼 갭을 둔다.
  • 코드 라인 34에서 GICR_TYPER.LAST 비트가 0이면 아직 끝나지 않았으므로 다음을 처리하기 위해 계속 루프를 돈다.

 

다음 그림은 4개의 노드에 각각 16개의 cpu를 사용하여 Redistributor를 구성한 모습을 보여준다.

  • Redistributor 레지스터 세트는 각각 256K를 사용하였고, 전체 64 세트이다. (4 redistributor regions * 16 cpus)

 

다음 그림은 1개의 노드에 big/little 클러스터가 같이 구현되어 있는 Redistributor를 구성한 모습과 ppi 파티션이 구성된 모습을 보여준다.

  • Redistributor 레지스터 세트는 각각 128K를 사용하였고, 전체 6 세트이다. (1 redistributor regions * 6 cpus)
  • 2 개의 파티션에 각각 4개 cpu와 2개 cpu가 구성되어 있다.

 

__gic_update_rdist_properties()

drivers/irqchip/irq-gic-v3.c

static int __gic_update_rdist_properties(struct redist_region *region,
                                         void __iomem *ptr)
{
        u64 typer = gic_read_typer(ptr + GICR_TYPER);
        gic_data.rdists.has_vlpis &= !!(typer & GICR_TYPER_VLPIS);
        gic_data.rdists.has_direct_lpi &= !!(typer & GICR_TYPER_DirectLPIS);
        gic_data.ppi_nr = min(GICR_TYPER_NR_PPIS(typer), gic_data.ppi_nr);

        return 1;
}

Redistributor 레지스터를 읽어 속성 갑을 갱신한다.

  • 코드 라인 4에서 Redistributor 레지스터 베이스를 가리키는 ptr을 기반으로 GICR_TYPER 레지스터 값을 읽어온다.
  • 코드 라인 5에서 GICR_TYPER.VLPIS가 하나라도 0이면 false 설정으로 바꾼다.
  • 코드 라인 6에서 GICR_TYPER.DirectLPIS가 하나라도 0이면 false 설정으로 바꾼다.
  • 코드 라인 7에서 GIC를 통해 읽은 PPI 수와 GICR을 통해 읽은 PPI 수 중 작은 값으로 ppi_nr에 대입한다.

 

Redistributor 활성화

gic_populate_rdist()

drivers/irqchip/irq-gic-v3.c

static int gic_populate_rdist(void)
{
        if (gic_iterate_rdists(__gic_populate_rdist) == 0)
                return 0;

        /* We couldn't even deal with ourselves... */
        WARN(true, "CPU%d: mpidr %lx has no re-distributor!\n",
             smp_processor_id(),
             (unsigned long)cpu_logical_map(smp_processor_id()));
        return -ENODEV;
}

Redistributor들에서 cpu의 affinity와 동일한 경우 rd_base와 phys_base를 갱신한다.

 

__gic_populate_rdist()

drivers/irqchip/irq-gic-v3.c

static int __gic_populate_rdist(struct redist_region *region, void __iomem *ptr)
{
        unsigned long mpidr = cpu_logical_map(smp_processor_id());
        u64 typer;
        u32 aff;

        /*
         * Convert affinity to a 32bit value that can be matched to
         * GICR_TYPER bits [63:32].
         */
        aff = (MPIDR_AFFINITY_LEVEL(mpidr, 3) << 24 |
               MPIDR_AFFINITY_LEVEL(mpidr, 2) << 16 |
               MPIDR_AFFINITY_LEVEL(mpidr, 1) << 8 |
               MPIDR_AFFINITY_LEVEL(mpidr, 0));

        typer = gic_read_typer(ptr + GICR_TYPER);
        if ((typer >> 32) == aff) {
                u64 offset = ptr - region->redist_base;
                gic_data_rdist_rd_base() = ptr;
                gic_data_rdist()->phys_base = region->phys_base + offset;

                pr_info("CPU%d: found redistributor %lx region %d:%pa\n",
                        smp_processor_id(), mpidr,
                        (int)(region - gic_data.redist_regions),
                        &gic_data_rdist()->phys_base);
                return 0;
        }

        /* Try next one */
        return 1;
}

MPIDR을 통해 알아온 4단계 affinity 값과 GICR_TYPER을 통해 읽어온 4 단계 affinity 값이 같은 경우에 한해 rd_base와 phys_base를 갱신하고 로그 정보를 출력한다. 성공 시 0을 반환한다.

  • GICR_TYPER 레지스터는 64비트 값을 가지고 있고 bits[61:32]에 4단계 affinity 값이 담겨있다.

 

gic_enable_redist()

drivers/irqchip/irq-gic-v3.c

static void gic_enable_redist(bool enable)
{
        void __iomem *rbase;
        u32 count = 1000000;    /* 1s! */
        u32 val;

        if (gic_data.flags & FLAGS_WORKAROUND_GICR_WAKER_MSM8996)
                return;

        rbase = gic_data_rdist_rd_base();

        val = readl_relaxed(rbase + GICR_WAKER);
        if (enable)
                /* Wake up this CPU redistributor */
                val &= ~GICR_WAKER_ProcessorSleep;
        else
                val |= GICR_WAKER_ProcessorSleep;
        writel_relaxed(val, rbase + GICR_WAKER);

        if (!enable) {          /* Check that GICR_WAKER is writeable */
                val = readl_relaxed(rbase + GICR_WAKER);
                if (!(val & GICR_WAKER_ProcessorSleep))
                        return; /* No PM support in this redistributor */
        }

        while (--count) {
                val = readl_relaxed(rbase + GICR_WAKER);
                if (enable ^ (bool)(val & GICR_WAKER_ChildrenAsleep))
                        break;
                cpu_relax();
                udelay(1);
        };
        if (!count)
                pr_err_ratelimited("redistributor failed to %s...\n",
                                   enable ? "wakeup" : "sleep");
}

Redistributor를 깨우거나 슬립시킨다. @enable=1일 때 깨우고, @enable=0일 때 슬립한다.

  • 코드 라인 7~8에서 FLAGS_WORKAROUND_GICR_WAKER_MSM8996 워크어라운드 플래그가 설정된 경우 함수를 빠져나간다.
  • 코드 라인 10~18에서 GICR_WAKER.ProcessorSleep 필드를 제거(@enable=1)하거나 추가(@enable=0)한다.
  • 코드 라인 20~24에서 @enable=0일 때 GICR_WAKER.ProcessorSleep 을 다시 읽어 0인 경우 PM을 지원하지 않으므로 함수를 빠져나간다.
  • 코드 라인 26~32에서 최대 1초간 busy wait하며 GICR_WAKER.ChildrenAsleep을 읽은 값과 enable이 서로 다른 경우 루프를 벗어난다.
  • 코드 라인 33~35에서 1초 동안 설정되지 않은 경우 “Redistributor failed to (wakeup|sleep)”을 출력한다.

 


KVM 지원

gic_of_setup_kvm_info()

irq-gic-v3.c

static void __init gic_of_setup_kvm_info(struct device_node *node)
{
        int ret;
        struct resource r;
        u32 gicv_idx;

        gic_v3_kvm_info.type = GIC_V3;

        gic_v3_kvm_info.maint_irq = irq_of_parse_and_map(node, 0);
        if (!gic_v3_kvm_info.maint_irq)
                return;

        if (of_property_read_u32(node, "#redistributor-regions",
                                 &gicv_idx))
                gicv_idx = 1;

        gicv_idx += 3;  /* Also skip GICD, GICC, GICH */
        ret = of_address_to_resource(node, gicv_idx, &r);
        if (!ret)
                gic_v3_kvm_info.vcpu = r;

        gic_v3_kvm_info.has_v4 = gic_data.rdists.has_vlpis;
        gic_set_kvm_info(&gic_v3_kvm_info);
}

KVM을 통해 Guest OS를 동작시킬 때 사용할 gic 정보를 저장한다.

 

gic_set_kvm_info()

drivers/irqchip/irq-gic-common.c

void gic_set_kvm_info(const struct gic_kvm_info *info)
{
        BUG_ON(gic_kvm_info != NULL);
        gic_kvm_info = info;
}

virt/kvm/arm/vgic/vgic-init.c – kvm_vgic_hyp_init() 함수에서 gic-v3가 제공한 정보를 가져다 virtual gic를 초기화할 떄 사용한다.

 


GIC v3 Operations

GIC v3용 irq_chip Operations는 가상화 지원 관련하여 두 가지가 사용된다.

  • gic_eoimode1_chip
    • 호스트 OS용
  • gic_chip
    • Guest OS용

 

호스트 OS용 gic_eoimode1_chip

drivers/irqchip/irq-gic-v3.c

static struct irq_chip gic_eoimode1_chip = {
        .name                   = "GICv3",
        .irq_mask               = gic_eoimode1_mask_irq,
        .irq_unmask             = gic_unmask_irq,
        .irq_eoi                = gic_eoimode1_eoi_irq,
        .irq_set_type           = gic_set_type,
        .irq_set_affinity       = gic_set_affinity,
        .irq_get_irqchip_state  = gic_irq_get_irqchip_state,
        .irq_set_irqchip_state  = gic_irq_set_irqchip_state,
        .irq_set_vcpu_affinity  = gic_irq_set_vcpu_affinity,
        .irq_nmi_setup          = gic_irq_nmi_setup,
        .irq_nmi_teardown       = gic_irq_nmi_teardown,
        .flags                  = IRQCHIP_SET_TYPE_MASKED |
                                  IRQCHIP_SKIP_SET_WAKE |
                                  IRQCHIP_MASK_ON_SUSPEND,
};

 

Guest OS용 gic_chip

drivers/irqchip/irq-gic-v3.c

static struct irq_chip gic_chip = {
        .name                   = "GICv3",
        .irq_mask               = gic_mask_irq,
        .irq_unmask             = gic_unmask_irq,
        .irq_eoi                = gic_eoi_irq,
        .irq_set_type           = gic_set_type,
        .irq_set_affinity       = gic_set_affinity,
        .irq_get_irqchip_state  = gic_irq_get_irqchip_state,
        .irq_set_irqchip_state  = gic_irq_set_irqchip_state,
        .irq_nmi_setup          = gic_irq_nmi_setup,
        .irq_nmi_teardown       = gic_irq_nmi_teardown,
        .flags                  = IRQCHIP_SET_TYPE_MASKED |
                                  IRQCHIP_SKIP_SET_WAKE |
                                  IRQCHIP_MASK_ON_SUSPEND,
};

 

인터럽트 Mask & Unmask

gic_eoimode1_mask_irq()

drivers/irqchip/irq-gic-v3.c

static void gic_eoimode1_mask_irq(struct irq_data *d)
{
        gic_mask_irq(d);
        /*
         * When masking a forwarded interrupt, make sure it is
         * deactivated as well.
         *
         * This ensures that an interrupt that is getting
         * disabled/masked will not get "stuck", because there is
         * noone to deactivate it (guest is being terminated).
         */
        if (irqd_is_forwarded_to_vcpu(d))
                gic_poke_irq(d, GICD_ICACTIVER);
}

해당 인터럽트를 mask 한다.

  • 코드 라인 3에서 GICD_ICENABLERn의 인터럽트 번호에 해당하는 비트에 1을 기록하여 physical 인터럽트를 mask 한다.
  • 코드 라인 12~13에서 Guest OS에 vcpu를 통해 인터럽트를 전달 가능한 경우 GICD_ICACTIVERn의 인터럽트 번호에 해당하는 비트에 1을 기록하여 deactivate 요청을 한다.

 

gic_mask_irq()

drivers/irqchip/irq-gic-v3.c

static void gic_mask_irq(struct irq_data *d)
{
        gic_poke_irq(d, GICD_ICENABLER);
}

해당 인터럽트를 mask 한다.

  • 코드 라인 3에서 GICD_ICENABLERn의 인터럽트 번호에 해당하는 비트에 1을 기록하여 physical 인터럽트를 mask 한다.

 

 

gic_unmask_irq()

drivers/irqchip/irq-gic-v3.c

static void gic_unmask_irq(struct irq_data *d)
{
        gic_poke_irq(d, GICD_ISENABLER);
}

해당 인터럽트를 unmask 한다.

  • 코드 라인 3에서 GICD_ISENABLERn의 인터럽트 번호에 해당하는 비트에 1을 기록하여 physical 인터럽트를 unmask 한다.

 

레지스터 Peek & Poke

gic_peek_irq()

drivers/irqchip/irq-gic-v3.c

static int gic_peek_irq(struct irq_data *d, u32 offset)
{
        void __iomem *base;
        u32 index, mask;

        offset = convert_offset_index(d, offset, &index);
        mask = 1 << (index % 32);

        if (gic_irq_in_rdist(d))
                base = gic_data_rdist_sgi_base();
        else
                base = gic_data.dist_base;

        return !!(readl_relaxed(base + offset + (index / 32) * 4) & mask);
}

@offset 위치 레지스터의 해당 인터럽트에 대응하는 비트 값을 읽어온다. (1=설정, 0=클리어)

  • 코드 라인 6에서 해당 인터럽트의 @offset에 해당하는 offset을 알아오고, @index에는 세 가지(PPI/SPI, EPPI 및 ESPI) 타입의 0부터 시작하는 값을 알아온다.
  • 코드 라인 9~12에서 해당 인터럽트가 Redistributor를 사용해야 하는 per-cpu 타입(PPI/EPPI) 여부에 따라 베이스 주소를 sgi_base로, 그 외의 경우 dist_base로 한다.
  • 코드 라인 14에서 레지스터 베이스에 offset을 더한 주소에서 산출된 mask 비트 값을 읽어온다.

 

gic_poke_irq()

drivers/irqchip/irq-gic-v3.c

static void gic_poke_irq(struct irq_data *d, u32 offset)
{
        void (*rwp_wait)(void);
        void __iomem *base;
        u32 index, mask;

        offset = convert_offset_index(d, offset, &index);
        mask = 1 << (index % 32);

        if (gic_irq_in_rdist(d)) {
                base = gic_data_rdist_sgi_base();
                rwp_wait = gic_redist_wait_for_rwp;
        } else {
                base = gic_data.dist_base;
                rwp_wait = gic_dist_wait_for_rwp;
        }

        writel_relaxed(mask, base + offset + (index / 32) * 4);
        rwp_wait();
}

@offset 위치 레지스터의 해당 인터럽트에 대응하는 비트에 1을 기록한다.

  • clear-request 또는 set-request 타입 레지스터에 사용한다

 

convert_offset_index()

drivers/irqchip/irq-gic-v3.c

/*
 * Routines to disable, enable, EOI and route interrupts
 */
static u32 convert_offset_index(struct irq_data *d, u32 offset, u32 *index)
{
        switch (get_intid_range(d)) {
        case PPI_RANGE:
        case SPI_RANGE:
                *index = d->hwirq;
                return offset;
        case EPPI_RANGE:
                /*
                 * Contrary to the ESPI range, the EPPI range is contiguous
                 * to the PPI range in the registers, so let's adjust the
                 * displacement accordingly. Consistency is overrated.
                 */
                *index = d->hwirq - EPPI_BASE_INTID + 32;
                return offset;
        case ESPI_RANGE:
                *index = d->hwirq - ESPI_BASE_INTID;
                switch (offset) {
                case GICD_ISENABLER:
                        return GICD_ISENABLERnE;
                case GICD_ICENABLER:
                        return GICD_ICENABLERnE;
                case GICD_ISPENDR:
                        return GICD_ISPENDRnE;
                case GICD_ICPENDR:
                        return GICD_ICPENDRnE;
                case GICD_ISACTIVER:
                        return GICD_ISACTIVERnE;
                case GICD_ICACTIVER:
                        return GICD_ICACTIVERnE;
                case GICD_IPRIORITYR:
                        return GICD_IPRIORITYRnE;
                case GICD_ICFGR:
                        return GICD_ICFGRnE;
                case GICD_IROUTER:
                        return GICD_IROUTERnE;
                default:
                        break;
                }
                break;
        default:
                break;
        }

        WARN_ON(1);
        *index = d->hwirq;
        return offset;
}

해당 인터럽트 타입과 @offset 주소로 레지스터에 대한 offset을 알아온다. 그리고 출력 인자 *index에는 hwirq 번호를 대입한다.

  • 코드 라인 3~7에서 해당 인터럽트가 PPI 및 SPI 범위인 경우 @offset을 그대로 반환하고, @index에는 d->hwirq역시 그대로 반환한다.
  • 코드 라인 8~15에서 해당 인터럽트가 확장 PPI 범위인 경우 @offset을 그대로 반환하고, @index에는 d->hwirq – EPPI_BASE_INTID(1024 + 32) + 32 값을 반환한다.
    • 결국 @index = d->hwirq – 1024 (확장 SPI에 대한 @index는 0 ~ 1023)
  • 코드 라인 16~36에서 해당 인터럽트가 확장 SPI 범위인 경우 @offset에 따라 레지스터 offset을 반환하고, @index에는 d->hwirq – ESPI_BASE_INTID 값을 반환한다.
    • 결국 @index = d->hwirq – 4096 (확장 SPI의 INTID 는 4096 부터 시작, @index는 0부터)
  • 코드 라인 37~47에서 그 외의 범위인 경우 @offset을 그대로 반환하고, @index에는 d->hwirq역시 그대로 반환한다.

 

인터럽트 타입 설정

gic_set_type()

drivers/irqchip/irq-gic-v3.c

static int gic_set_type(struct irq_data *d, unsigned int type)
{
        enum gic_intid_range range;
        unsigned int irq = gic_irq(d);
        void (*rwp_wait)(void);
        void __iomem *base;
        u32 offset, index;
        int ret;

        /* Interrupt configuration for SGIs can't be changed */
        if (irq < 16)
                return -EINVAL;

        range = get_intid_range(d);

        /* SPIs have restrictions on the supported types */
        if ((range == SPI_RANGE || range == ESPI_RANGE) &&
            type != IRQ_TYPE_LEVEL_HIGH && type != IRQ_TYPE_EDGE_RISING)
                return -EINVAL;

        if (gic_irq_in_rdist(d)) {
                base = gic_data_rdist_sgi_base();
                rwp_wait = gic_redist_wait_for_rwp;
        } else {
                base = gic_data.dist_base;
                rwp_wait = gic_dist_wait_for_rwp;
        }

        offset = convert_offset_index(d, GICD_ICFGR, &index);

        ret = gic_configure_irq(index, type, base + offset, rwp_wait);
        if (ret && (range == PPI_RANGE || range == EPPI_RANGE)) {
                /* Misconfigured PPIs are usually not fatal */
                pr_warn("GIC: PPI INTID%d is secure or misconfigured\n", irq);
                ret = 0;
        }

        return ret;
}

해당 인터럽트에 대한 트리거 타입을 지정한다.

  • 코드 라인 11~12에서 SGI는 에러를 반환한다.
  • 코드 라인 14~19에서 SPI 또는 확장 SPI 영역인 경우 level high 또는 edge rising 트리거 타입만 지정할 수 있다.
  • 코드 라인 21~27에서 해당 인터럽트가 Redistributor를 사용해야 하는 per-cpu 타입(PPI/EPPI) 여부에 따라 sgi_base를 그 외의 경우 dist_base를 레지스터 베이스로 지정한다.
  • 코드 라인 29에서 PPI/SPI인 경우 GICD_ICFGRn 또는 EPPI/ESPI인 경우 GICD_ICFGRnE 레지스터에 대한 offset 주소를 알아오고 index에는 해당 인터럽트에 대한 0부터 시작하는 인덱스 값을 알아온다.(n)
  • 코드 라인 31~36에서 위에서 알아온 베이스 주소 + offset 주소의 레지스터에 index에 해당하는 인터럽트 위치에 type을 기록하고, rwp_wait 함수를 통해 레지스터에 기록이 완료될 때까지 대기한다.

 

get_intid_range()

drivers/irqchip/irq-gic-v3.c

static enum gic_intid_range get_intid_range(struct irq_data *d)
{
        return __get_intid_range(d->hwirq);
}

해당 인터럽트에 대한 레인지 타입을 알아온다.

 

__get_intid_range()

drivers/irqchip/irq-gic-v3.c

static enum gic_intid_range __get_intid_range(irq_hw_number_t hwirq)
{
        switch (hwirq) {
        case 16 ... 31:
                return PPI_RANGE;
        case 32 ... 1019:
                return SPI_RANGE;
        case EPPI_BASE_INTID ... (EPPI_BASE_INTID + 63):
                return EPPI_RANGE;
        case ESPI_BASE_INTID ... (ESPI_BASE_INTID + 1023):
                return ESPI_RANGE;
        case 8192 ... GENMASK(23, 0):
                return LPI_RANGE;
        default:
                return __INVALID_RANGE__;
        }
}

@hwirq 값에 따른 인터럽트에 대한 레인지 타입을 반환한다.

  • PPI_RANGE: hwirq 16 ~ 31
  • SPI_RANGE: hwirq: 32 ~ 1019
  • EPPI_RANGE: 1024 ~ 1024 + 63
  • ESPI_RANGE: 4096 ~ 4096 + 1023
  • LPI_RANGE: 8192 ~ 0x80_0000 (2^23)

 

gic_intid_range

drivers/irqchip/irq-gic-v3.c

enum gic_intid_range {
        PPI_RANGE,
        SPI_RANGE,
        EPPI_RANGE,
        ESPI_RANGE,
        LPI_RANGE,
        __INVALID_RANGE__
};

 

gic_configure_irq()

drivers/irqchip/irq-gic-common.c

int gic_configure_irq(unsigned int irq, unsigned int type,
                       void __iomem *base, void (*sync_access)(void))
{
        u32 confmask = 0x2 << ((irq % 16) * 2);
        u32 confoff = (irq / 16) * 4;
        u32 val, oldval;
        int ret = 0;
        unsigned long flags;

        /*
         * Read current configuration register, and insert the config
         * for "irq", depending on "type".
         */
        raw_spin_lock_irqsave(&irq_controller_lock, flags);
        val = oldval = readl_relaxed(base + confoff);
        if (type & IRQ_TYPE_LEVEL_MASK)
                val &= ~confmask;
        else if (type & IRQ_TYPE_EDGE_BOTH)
                val |= confmask;

        /* If the current configuration is the same, then we are done */
        if (val == oldval) {
                raw_spin_unlock_irqrestore(&irq_controller_lock, flags);
                return 0;
        }

        /*
         * Write back the new configuration, and possibly re-enable
         * the interrupt. If we fail to write a new configuration for
         * an SPI then WARN and return an error. If we fail to write the
         * configuration for a PPI this is most likely because the GIC
         * does not allow us to set the configuration or we are in a
         * non-secure mode, and hence it may not be catastrophic.
         */
        writel_relaxed(val, base + confoff);
        if (readl_relaxed(base + confoff) != val)
                ret = -EINVAL;

        raw_spin_unlock_irqrestore(&irq_controller_lock, flags);

        if (sync_access)
                sync_access();

        return ret;
}

@irq에 해당하는 2 비트의 @type을 일반/확장 인터럽트 여부에 따라 GICD_TYPERn 또는 GICD_TYPERnE에 기록한다. 기록한 후에는 기록이 완료될 때까지 잠시 대기한다.

 


Pseudo-NMI 지원

Pseudo-NMI 초기화

gic_enable_nmi_support()

drivers/irqchip/irq-gic-v3.c

static void gic_enable_nmi_support(void)
{
        int i;

        if (!gic_prio_masking_enabled())
                return;

        if (gic_has_group0() && !gic_dist_security_disabled()) {
                pr_warn("SCR_EL3.FIQ is cleared, cannot enable use of pseudo-NMIs\n");
                return;
        }

        ppi_nmi_refs = kcalloc(gic_data.ppi_nr, sizeof(*ppi_nmi_refs), GFP_KERNEL);
        if (!ppi_nmi_refs)
                return;

        for (i = 0; i < gic_data.ppi_nr; i++)
                refcount_set(&ppi_nmi_refs[i], 0);

        static_branch_enable(&supports_pseudo_nmis);

        if (static_branch_likely(&supports_deactivate_key))
                gic_eoimode1_chip.flags |= IRQCHIP_SUPPORTS_NMI;
        else
                gic_chip.flags |= IRQCHIP_SUPPORTS_NMI;
}

Pesudo-NMI를 지원하는 경우 초기화 한다.

  • 코드 라인 5~6에서 Pesudo-NMI 기능을 위해 시스템이 irq masking 기능이 필요한데 사용할 수 없으면 그냥 함수를 빠져나간다.
  • 코드 라인 8~11에서 커널에서 gorup 0를 지원하면서 secure를 동작 중인 경우 경고 메시지를 출력하고 함수를 빠져나간다.
    • EL3에서 secure가 동작 중이면 SCR_EL3.FIQ가 설정되어 FIQ를 커널에서 처리해야 한다.
  • 코드 라인 13~15에서 ppi_nmi_refs 레퍼런스 카운터를 ppi 수 만큼 할당한다.
  • 코드 라인 17~18에서 ppi_nmi_refs[]를 모두 0으로 설정한다.
  • 코드 라인 20에서 Pesudo-NMI 기능이 동작한다는 static key를 활성화한다.
  • 코드 라인 22~25에서 EL2 호스트 OS 운영에 따라 irq_chip operations의 플래그에 NMI 지원을 추가한다.

 

Pseudo-NMI 인터럽트 설정

gic_irq_nmi_setup()

drivers/irqchip/irq-gic-v3.c

static int gic_irq_nmi_setup(struct irq_data *d)
{
        struct irq_desc *desc = irq_to_desc(d->irq);

        if (!gic_supports_nmi())
                return -EINVAL;

        if (gic_peek_irq(d, GICD_ISENABLER)) {
                pr_err("Cannot set NMI property of enabled IRQ %u\n", d->irq);
                return -EINVAL;
        }

        /*
         * A secondary irq_chip should be in charge of LPI request,
         * it should not be possible to get there
         */
        if (WARN_ON(gic_irq(d) >= 8192))
                return -EINVAL;

        /* desc lock should already be held */
        if (gic_irq_in_rdist(d)) {
                u32 idx = gic_get_ppi_index(d);

                /* Setting up PPI as NMI, only switch handler for first NMI */
                if (!refcount_inc_not_zero(&ppi_nmi_refs[idx])) {
                        refcount_set(&ppi_nmi_refs[idx], 1);
                        desc->handle_irq = handle_percpu_devid_fasteoi_nmi;
                }
        } else {
                desc->handle_irq = handle_fasteoi_nmi;
        }

        gic_irq_set_prio(d, GICD_INT_NMI_PRI);

        return 0;
}

해당 인터럽트를 Pesudo-NMI로 설정한다.

  • 코드 라인 5~6에서 Pesudo-NMI를 지원하지 않는 시스템인 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 8~11에서 해당 인터럽트가 이미 enable 된 경우 에러 메시지를 출력하고 -EINVAL 에러를 반환한다.
  • 코드 라인 17~18에서 LPI인 경우 지원하지 않으므로 -EINVAL을 반환한다.
  • 코드 라인 21~31에서 해당 인터럽트가 Redistributor를 사용해야 하는 per-cpu 타입(PPI/EPPI) 여부에 따라 다음과 같이 설정한다.
    • PPI/EPPI 타입인 경우 해당 인터럽트 번호별로 첫 번째 설정인 경우에만 핸들러를 handle_percpu_devid_fasteoi_nmi() 함수로 설정한다.
    • 그 외 타입인 경우 핸들러를 handle_fasteoi_nmi() 함수로 지정한다.
    • 참고: Interrupts -2- (irq chip) | 문c
  • 코드 라인 33에서 인터럽트 우선 순위 값을 0x20 (내부 변환 후 0x90)으로 설정한다.
    • non-secure EL1에서 동작하는 커널에서 우선 순위 값을 설정하면 group 1 인터럽트들만 설정 가능하며 다음과 같은 기준으로 변경되어 설정된다.
      • prio(0x20)  >> 1 | 0x80 = 변환 prio(0x90)

 

gic_supports_nmi()

drivers/irqchip/irq-gic-v3.c

static inline bool gic_supports_nmi(void)
{
        return IS_ENABLED(CONFIG_ARM64_PSEUDO_NMI) &&
               static_branch_likely(&supports_pseudo_nmis);
}

Pesudo-NMI를 지원하는지 여부를 반환한다.

 

gic_irq_in_rdist()

drivers/irqchip/irq-gic-v3.c

static inline int gic_irq_in_rdist(struct irq_data *d)
{
        enum gic_intid_range range = get_intid_range(d);
        return range == PPI_RANGE || range == EPPI_RANGE;
}

해당 인터럽트가 Redistributor를 사용해야 하는지 여부를 알아온다. (PPI 또는 EPPI인 경우)

 

인터럽트 우선 순위 설정

gic_irq_set_prio()

drivers/irqchip/irq-gic-v3.c

static void gic_irq_set_prio(struct irq_data *d, u8 prio)
{
        void __iomem *base = gic_dist_base(d);
        u32 offset, index;

        offset = convert_offset_index(d, GICD_IPRIORITYR, &index);

        writeb_relaxed(prio, base + offset + index);
}

해당 인터럽트의 우선 순위를 설정한다.

  • non-secure EL1에서 동작하는 커널에서 우선 순위 값을 설정하면 group 1 인터럽트들만 설정 가능하며 다음과 같은 기준으로 변경되어 설정된다.
    • prio  >> 1 | 0x80 = 변환 prio (cpu interface로 전달되는 최종 priority 값이다)

 

Pseudo-NMI 인터럽트 해제

kernel/irq/chip.c

static void gic_irq_nmi_teardown(struct irq_data *d)
{
        struct irq_desc *desc = irq_to_desc(d->irq);

        if (WARN_ON(!gic_supports_nmi()))
                return;

        if (gic_peek_irq(d, GICD_ISENABLER)) {
                pr_err("Cannot set NMI property of enabled IRQ %u\n", d->irq);
                return;
        }

        /*
         * A secondary irq_chip should be in charge of LPI request,
         * it should not be possible to get there
         */
        if (WARN_ON(gic_irq(d) >= 8192))
                return;

        /* desc lock should already be held */
        if (gic_irq_in_rdist(d)) {
                u32 idx = gic_get_ppi_index(d);

                /* Tearing down NMI, only switch handler for last NMI */
                if (refcount_dec_and_test(&ppi_nmi_refs[idx]))
                        desc->handle_irq = handle_percpu_devid_irq;
        } else {
                desc->handle_irq = handle_fasteoi_irq;
        }

        gic_irq_set_prio(d, GICD_INT_DEF_PRI);
}

해당 인터럽트를 Pesudo-NMI에서 일반 인터럽트로 변경하여 설정한다.

  • 코드 라인 5~6에서 Pesudo-NMI를 지원하지 않는 시스템인 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 8~11에서 해당 인터럽트가 이미 enable 된 경우 에러 메시지를 출력하고 -EINVAL 에러를 반환한다.
  • 코드 라인 17~18에서 LPI인 경우 지원하지 않으므로 -EINVAL을 반환한다.
  • 코드 라인 21~29에서 해당 인터럽트가 Redistributor를 사용해야 하는 per-cpu 타입(PPI/EPPI) 여부에 따라 다음과 같이 설정한다.
    • PPI/EPPI 타입인 경우 해당 인터럽트 번호별로 첫 번째 설정인 경우에만 핸들러를 handle_percpu_devid_irq() 함수로 설정한다.
    • 그 외 타입인 경우 핸들러를 handle_fasteoi_irq() 함수로 지정한다.
  • 코드 라인 31에서 인터럽트 우선 순위 값을 0xa0 (내부 변환 후 0xd0)으로 설정한다.
    • non-secure EL1에서 동작하는 커널에서 우선 순위 값을 설정하면 group 1 인터럽트들만 설정 가능하며 다음과 같은 기준으로 변경되어 설정된다.
      • prio(0xa0)  >> 1 | 0x80 = 변환 prio(0xd0)

 


구조체

gic_chip_data 구조체

drivers/irqchip/irq-gic-v3.c

struct gic_chip_data {
        struct fwnode_handle    *fwnode;
        void __iomem            *dist_base;
        struct redist_region    *redist_regions;
        struct rdists           rdists;
        struct irq_domain       *domain;
        u64                     redist_stride;
        u32                     nr_redist_regions;
        u64                     flags;
        bool                    has_rss;
        unsigned int            ppi_nr;
        struct partition_desc   **ppi_descs;
};

GIC v3 컨트롤러의 내부 칩 데이터를 구성한다.

  • *fwnode
    • 펌웨어 노드 정보
  • *dist_base
    • Distributor 영역 가상 주소 베이스
  • *redist_regions
    • Redistributor 영역 수 만큼 redist_region 구조체를 할당하여 사용한다.
  • *domain
    • GIC가 사용하는 irq domain을 가리킨다.
  • redist_stride
    • Distributor들간의 주소 간격이 지정된다.
  • nr_redist_regions
    • Redistributor 영역 수
  • flags
  • has_rss
    • affinity 0 레벨에서 0~255 범위의 cpu 번호를 지원해야 하는지 여부 (Range Select Support)
  • **ppi_descs
    • PPI 파티션을 사용하는 경우 파티션 수 만큼 partition_desc 구조체를 할당하여 사용한다.

 

redist_region 구조체

drivers/irqchip/irq-gic-v3.c

struct redist_region {
        void __iomem            *redist_base;
        phys_addr_t             phys_base;
        bool                    single_redist;
};

하나의 Redistributor 정보를 관리한다.

  •  *redist_base
    • Redistributor 레지스터 가상 주소 베이스
  • phys_base
    • Redistributor 레지스터 물리 주소 베이스
  • single_redist
    • Redistributor 영역 내에 single Redistributor인지 여부

 

rdists 구조체

include/linux/irqchip/arm-gic-v3.h

struct rdists {
        struct {
                void __iomem    *rd_base;
                struct page     *pend_page;
                phys_addr_t     phys_base;
                bool            lpi_enabled;
        } __percpu              *rdist;
        phys_addr_t             prop_table_pa;
        void                    *prop_table_va;
        u64                     flags;
        u32                     gicd_typer;
        bool                    has_vlpis;
        bool                    has_direct_lpi;
};

하나의 Redistributor region 정보를 관리한다.

  •  *rdist
    • cpu 수 만큼 다음 4 항목을 구성한다.
  • *rd_base
    • Redistributor 레지스터 가상 주소 베이스
  • *pend_page
  • phys_base
    • Redistributor 레지스터 물리 주소 베이스
  • lpi_enabled
    • LPI 활성화 여부
  • prop_table_pa
  • *prop_table_va
  • flags
  • gicd_typer
  • has_vlpis
    • VLPIS 지원 여부
    • for GIC v4
  • has_direct_lpi
    • Direct LPI 지원 여부

 

참고

 

 

Timer -6- (Clocksource & Timer Driver)

<kernel v5.4>

Timer -6- (Clock Source & Timer Driver)

 

리눅스는 clock sources subsystem에 등록된 클럭 소스들 중 가장 정확도가 높고 안정적인 클럭 소스를 찾아 제공한다. 이 클럭 소스를 읽어 리눅스 시간 관리를 수행하는 timekeeping  subsystem에 제공된다.

  • 주요 연관 관계
    • clk(common clock foundation) -> clock sources subsystem -> timekeeping subsystem

 

또한 타이머를 사용하기 위해 커널은 timer 및 hrtimer API를 제공하고, 이의 타이머 hw 제어를 위해 최대한 아키텍처 독립적인 코드를 사용하여 clock events subsystem에 제공된다.

  • 주요 연관 관계
    • 타이머 프로그램: hrtimer API -> clock events susbsystem -> tick device -> timer hw
    • 타이머 만료: clk(common clock foundation) ->timer hw -> interrupt controller hw -> irq subsystem -> hrtimer API로 등록한 콜백 함수

 

clock events subsystem은 다음 두 가지 운용 모드를 사용하여 커널에 타이머 이벤트를 전달할 수 있다.

  • periodic
    • 규칙적으로 타이머 인터럽트를 발생시킨다.
  • oneshot
    • 단발 타이머 인터럽트가 발생할 수 있도록 만료시간을 프로그램할 수 있다.

 

다음 그림과 같이 1Mhz(1초에 100만번) 클럭 주파수를 사용하는 타이머 hw의 다운카운터에 10000 값을 설정하면, 10000 값이 0이 되기까지 10ms가 소요되며 그 후 타이머 인터럽트에가 발생되고, 이에 해당하는 타이머 ISR이 동작하는 것을 알 수 있다.

 

다음 그림은 각각의 cpu에 포함된 타이머에 기준 클럭을 공급하는 모습을 보여준다.

  • cpu마다 사용되는 타이머 인터럽트를 통해 타이머가 깨어나고 다음 타이머를 프로그램하는 과정이다.

 

타이머 클럭 주파수

타이머에 공급되는 클럭 주파수는 높으면 높을 수록 타이머의 해상도는 높아진다. 커널에 구현된 hrtimer는 1나노초 단위부터 제어 가능하다. 따라서 정밀도를 높이기 위해 이상적인 클럭 주파수는 1Ghz 이상을 사용하면 좋다. 그러나 외부 클럭 주파수가 높으면 높을 수록 전력 소모가 크므로 적절한 선으로 제한하여 사용한다. 현재 수 Mhz ~ 100Mhz의 클럭을 사용하며, 언젠가 필요에 의해 1Ghz 클럭도 사용되리라 판단된다. 저전력 시스템을 위해 500~20Khz를 사용하는 경우도 있다.

 

타이머 하드웨어 종류

타이머 하드웨어는 매우 다양하다. 이들을 크게 두 그룹으로 나눈다.

  • 아키텍처 내장 타이머 (architected timer)
    • arm per-core 아키텍처 내장 타이머 (rpi2, rpi3, rpi4)
    • arm 메모리 맵드 아키텍처 내장 타이머
  • 아키텍처 비내장 타이머 (SoC에 포함)
    • arm 글로벌 타이머
    • bcm2835 타이머 (rpi)

 

arm 아키텍처 내장형 generic 타이머 hw 타입

arm 아키텍처에 따라 generic timer extension이 적용되어 아키텍처에 내장된 generic 타이머들이다. 이 타이머는 SoC 제조 업체에서 설계에 따라 다음 2 가지 타입으로 나뉘어 사용된다. 스마트 폰등의 절전(deep sleep)을 위해 아키텍처 내장형 타이머는 주 타이머로 사용하고, 절전을 위해 메모리 매핑형 타이머를 브로드 캐스트 타이머로 나누어 동시에 동작시키기도 한다.

  • 코어별 아키텍처 내장 타이머(per-core architected timer)
    • per-cpu 보조프로세서 cp15를 통해 타이머 레지스터를 사용한다.
    • GIC의 PPIs를 사용하여 인터럽트를 전달한다.
    • armv7 및 armv8 아키텍처에서 사용된다.
  • 메모리 매핑형 아키텍처 내장 타이머(memory mapped architected timer)
    • 타이머 레지스터들을 주소 공간에 매핑하여 사용한다.
      • 장점으로 user 영역에서도 타이머에 접근할 수 있다.
    • 타이머당 8개까지의 프레임(타이머)을 사용할 수 있다.
    • GIC의 SPIs를 사용하여 인터럽트를 전달한다.
      • 특정 1개의 cpu만 인터럽트를 받을 수 있으므로 수신을 원하는 cpu로 인터럽트를 미리 라우팅해두어야 한다.
    • 틱 브로드 캐스트용 타이머로 사용한다.

 

코어별 아키텍처 내장 타이머 디바이스

rpi3용 “arm,armv7-timer” 타이머

arch/arm/boot/dts/bcm2837.dtsi

	timer {
		compatible = "arm,armv7-timer";
		interrupt-parent = <&local_intc>;
		interrupts = <0 IRQ_TYPE_LEVEL_HIGH>, // PHYS_SECURE_PPI
			     <1 IRQ_TYPE_LEVEL_HIGH>, // PHYS_NONSECURE_PPI
			     <3 IRQ_TYPE_LEVEL_HIGH>, // VIRT_PPI
			     <2 IRQ_TYPE_LEVEL_HIGH>; // HYP_PPI
		always-on;
	};

 

rpi4용 “arm,armv8-timer” 타이머

https://github.com/raspberrypi/linux/blob/rpi-4.19.y/arch/arm/boot/dts/bcm2838.dtsi

  • 아래 GIC_PPI 14은 rpi4가 사용하는 non-secure timer로 SGI에서 사용하는 irq 수 16을 더해 PPI ID30(hwirq=30)이다.
	timer {
		compatible = "arm,armv7-timer";
		interrupts = <GIC_PPI 13 (GIC_CPU_MASK_SIMPLE(4) |
					  IRQ_TYPE_LEVEL_LOW)>,
			     <GIC_PPI 14 (GIC_CPU_MASK_SIMPLE(4) |
					  IRQ_TYPE_LEVEL_LOW)>,
			     <GIC_PPI 11 (GIC_CPU_MASK_SIMPLE(4) |
					  IRQ_TYPE_LEVEL_LOW)>,
			     <GIC_PPI 10 (GIC_CPU_MASK_SIMPLE(4) |
					  IRQ_TYPE_LEVEL_LOW)>;
		arm,cpu-registers-not-fw-configured;
		always-on;
	};

arm,armv7-timer” 또는 “arm,armv8-timer” 타이머에 대한 속성의 용도는 다음과 같다.

  •  compatible
    • 타이머 디바이스 드라이버명 “arm,armv7-timer” 또는 “arm,armv8-timer”
  • interrupt-parent
    • 타이머 만료 시각에 local_intc 인터럽트 컨트롤러에 인터럽트가 발생된다.
  • interrupts
    • cpu core마다 최대 4개의 타이머가 사용될 수 있으며 각각에 대해 인터럽트가 발생된다. 각 타이머 인터럽트에 연결된 PPI 번호를 지정한다.
    • 4개 타이머 용도는 각각 다르다.
      • 시큐어 펌웨어용
      • non 시큐어용으로 커널이 사용
      • Guest 커널용
      • 하이퍼바이저용
  • always-on
    • 타이머에 c3 절전 기능이 없다. 즉 별도의 파워 블럭으로 관리되지 않고 항상 켜져있는 타이머이다.
    • suspend시에도 절대 타이머 전원이 다운되지 않는 시스템에서 이 속성을 사용한다.

다음은 옵션 파라미터이다.

  • clock-frequency
    • 타이머를 이용하기 위해 펌웨어가 CNTFREQ 레지스터에 클럭 주파수를 먼저 설정한다. 따라서 커널은 부팅 시 CNTFREQ 레지스터를 설정할 필요 없이 해당 레지스터의 값을 읽어오는 것으로 클럭 주파수를 알아올 수 있다. 만일 펌웨어가 CNTFREQ 레지스터에 클럭 주파수를 설정하지 않는 시스템을 사용하는 경우 이 속성 항목을 읽어 타이머를 초기화하여야 한다.가능하면 이 속성은 사용하지 않는 것을 권장한다.
  •  arm,cpu-registers-not-fw-configured
    • 펌웨어(주로 시큐어 펌웨어)가 어떠한 타이머 레지스터도 사용하지 않는 시스템이다. 이러한 경우 현재 커널이 시큐어용 타이머를 사용하도록 한다.
  •  arm,no-tick-in-suspend
    • 시스템 suspend(깊은 절전) 시 타이머가 stop 되는 기능이 있는 경우 사용된다.

 

메모리 매핑형 아키텍처 내장 타이머 디바이스

타이머 레지스터를 별도의 주소 공간에 매핑하므로 추가로 reg 속성을 사용하고, 최대 8개까지 frame을 사용한다.

arch/arm/boot/dts/qcom-apq8084.dtsi

soc: soc {
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;
        compatible = "simple-bus";

        timer@f9020000 {
                #address-cells = <1>;
                #size-cells = <1>;
                ranges;
                compatible = "arm,armv7-timer-mem";
                reg = <0xf9020000 0x1000>;
                clock-frequency = <19200000>;

                frame@f9021000 {
                        frame-number = <0>;
                        interrupts = <0 8 0x4>,
                                     <0 7 0x4>;
                        reg = <0xf9021000 0x1000>,
                              <0xf9022000 0x1000>;
                };

               (...생략...)

 


ARM per-core 아키텍처 내장 Generic Timer

다음 그림은 ARM SMP 각 core에 내장된 generic 타이머가 각 모드별로 사용하는 타이머 종류를 보여준다.

각 core 마다 최대 7개(4+3)개의 타이머가 인터럽트와 연결되어 있다.

  • EL1 physical timer
    • non 시큐어용으로 커널이 사용
      • 예) rpi4에서 부팅되는 메인 커널이 사용
  • EL1 virtual timer
    • Guest 커널용
      • 예) kvm용 guest 타이머
  • Non-secure EL2 physical timer
    • 하이퍼바이저용
      • 예) xen 하이퍼바이저
  • Non-secure EL2 virtual timer
    • ARMv8.1-VHE 구현시 사용 가능
  • Secure EL2 virtual timer
    • ARMv8.4-SecEL2 구현시 사용 가능
  • Secure EL2 physical timer
    • ARMv8.4-SecEL2 구현시 사용 가능
  • EL3 physical timer
    • 시큐어 펌웨어용
    • 시큐어 펌웨어를 사용하지 않는 경우 커널이 사용할 수도 있다.

 

다음 그림은 arm64 시스템에서 core별로 최대 7개의 타이머를 사용할 수 있음을 보여준다.

 

arm32 시스템에서는 core마다 최대 4개의 타이머를 사용할 수 있다.

  • Physical 타이머는 뱅크되어 3개의 모드에서 각각 사용된다.
    • PL1 Secure 모드
    • PL1 Non-Secure 모드
    • PL2 하이퍼 모드
  • Virtual 타이머는 guest OS에 사용되며 하이퍼 바이저가 설정하는 voffset을 더해 사용된다.

 

다음은 rpi4 시스템에서 사용 중인 타이머를 보여준다. 4개 각 cpu에 내장된 타이머이며 PPI ID30(hwirq=30)에 non-secure physical timer가 동작하고 있다.

  • PPI#27은 KVM을 사용하여 두 개의 cpu에 배정하여 동작시키고 있다.
$ cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3
  1:          0          0          0          0     GICv2  25 Level     vgic
  3:   18098867    2706362   12943752    3421084     GICv2  30 Level     arch_timer
  4:    4109642     550709          0          0     GICv2  27 Level     kvm guest timer
  8:        282          0          0          0     GICv2 114 Level     DMA IRQ
 16:    1645412          0          0          0     GICv2  65 Level     fe00b880.mailbox
 19:       6648          0          0          0     GICv2 153 Level     uart-pl011
 21:          0          0          0          0     GICv2 169 Level     brcmstb_thermal
 22:    3790111          0          0          0     GICv2 158 Level     mmc1, mmc0
 24:          0          0          0          0     GICv2  48 Level     arm-pmu
 25:          0          0          0          0     GICv2  49 Level     arm-pmu
 26:          0          0          0          0     GICv2  50 Level     arm-pmu
 27:          0          0          0          0     GICv2  51 Level     arm-pmu
 28:          0          0          0          0     GICv2 106 Level     v3d
 30:   27727409          0          0          0     GICv2 189 Level     eth0
 31:    3055759          0          0          0     GICv2 190 Level     eth0
 37:         32          0          0          0     GICv2  66 Level     VCHIQ doorbell
 39:    1921424          0          0          0  Brcm_MSI 524288 Edge      xhci_hcd
IPI0:   1796626    2697423    5510661    2638902       Rescheduling interrupts
IPI1:      8372     356892     286341     302455       Function call interrupts
IPI2:         0          0          0          0       CPU stop interrupts
IPI3:         0          0          0          0       CPU stop (for crash dump) interrupts
IPI4:         0          0          0          0       Timer broadcast interrupts
IPI5:   5243413     274725    4761277     692912       IRQ work interrupts
IPI6:         0          0          0          0       CPU wake-up interrupts

 

클럭 소스와 클럭 이벤트용 레지스터

클럭 소스에 사용되는 카운터 레지스터

  • 7개의 CNTxxCT_ELx 레지스터는 64비트 up-counter 레지스터로 클럭 주파수에 맞춰 계속 증가한다. 이 사이클 값을 읽어 리눅스 커널의 timekeeping 및 sched_clock 등에 제공하여 리눅스 시각과 태스크 소요 시간등을 갱신한다.

 

클럭 이벤트 프로그래밍에 사용되는 2 가지 타이머 레지스터

  • 첫 번째 방법
    • Timer value(7개의 CNTxxTVAL_ELx )레지스터는 32비트 down-counter로 클럭 주파수에 맞춰 감소하다 0이되면 타이머 인터럽트가 발생한다.
    • 인터럽트 조건 = timer value == 0
    • arm32, arm64 리눅스 커널은 이 레지스터를 사용하여 타이머를 동작시킨다.
  • 두 번째 방법
    • Compare value(7개의 CNTxxCVAL_ELx) 64비트 레지스터 값을 Counter(CNTxxCT_ELx) 레지스터 값이 같거나 초과하는 순간 타이머 인터럽트가 발생한다.
      • 인터럽트 조건 = counter – offset – compare value
      • Virtual 타이머의 경우 offset을 추가해야 한다.

 

다음 그림은 arm32에서 generic 타이머들의 구성을 보여준다.

  • 하이퍼 바이저용 Physical 카운터 관련 레지스터들(CNTHP_CTL, CNTHPCT, CNTHP_TVAL, CNTHP_CVAL) 생략
  • 이벤트 스트림 기능은 ARM 전용으로 atomic하게 busy wait 하는 delay 류의 API를 위해 ARM에서 절전을 위해 사용하는 wfe를 사용하는 중에 지속적으로 깨워 loop를 벗어나는 조건을 체크하기 위해 사용된다.

 


타이머 하드웨어

1) ARM 코어별 아키텍처 내장형 generic 타이머

대부분의 arm 및 arm64 시스템에서 사용하는 방법이다.

특징

  • 아키텍처 코어마다 2~4개의 타이머를 제공한다. (32bit down 카운터 및 64bit up 카운터를 제공)
    • no Security Extension & no Virutalization Extension
      • Physical 타이머 (커널용)
      • Virtual 타이머 (Guest OS용)
    • Security Extension only
      • Non-secure physical 타이머 (커널용)
      • Secure physical 타이머 (Secure 펌웨어용)
      • Virtual 타이머 (Guest OS용)
    • Security Extension & Virtualization Extension
      • Non-secure PL1 타이머 (커널용)
      • Secure PL1 physical 타이머 (Secure 펌웨어용)
      • Non-secure PL2 physical 타이머 (하이퍼바이저용)
      • Virtual 타이머 (Guest OS용)
  • Generic 타이머는 ARM 아키텍처가 Generic Timer Extension을 지원하는 경우에 사용할 수 있다.
    • ARMv7 및 ARMv8에 채용되었다.
  • 연속(continuous) 및 one shot event 지원
    • GIC의 PPI를 통해 만료 타임 시 인터럽트 시그널을 보낼 수 있다.
  • 고해상도 타이머(hrtimer)로 동작한다.
  • 타이머 레지스터에 접근하기 위해 코프로세서를 통해 사용할 수 있다.(CP14 & cp15)

 

다음 그림은 core별로 타이머가 내장되었고, GIC(Global Interrupt Controller)의 PPI를 통해 연결된 것을 확인할 수 있다.

 

Generic 타이머 레지스터

ARM64용 generic 타이머 레지스터

 

다음은 ARM64 generic 타이머 레지스터들 중 리눅스 커널과 관련된 레지스터들만 표현하였다.

  • CNTFRQ_EL0, Counter-timer Frequency register
    • bit[31..0]에 클럭 주파수가 설정된다.
    • 이 레지스터 값에 접근하려면 다음과 같이 코프로세서 명령을 사용한다.
      • MSR or MRC p15, 0, <Register>, c14, c0, 0
    • 예) 19.2Mhz의 클럭이 공급되는 경우 19200000 값이 설정된다.
  • CNTKCTL_EL1, Counter-timer Kernel Control register
    • EL0PTEN
      • EL0에서 physical 카운터 관련 레지스터들의 접근 설정. 0=disable, 1=enable
    • EL0VTEN
      • EL0에서 virtual 카운터 관련 레지스터들의 접근 설정. 0=disable, 1=eable
    • EVNTI
      • virtual 카운터로 트리거되는 이벤트 스트림 값
    • EVNTDIR
      • EVNTI 값으로 트리거될 때 방향. 0=0→1로 전환, 1→0 전환
    • EVNTEN
      • virtual 카운터로부터 event 스트림 생성. 0=disable, 1=enable
    • EL0VCTEN
      • PL0에서 virtual 카운터 및 frequency 레지스터의 접근 설정. 0=disable, 1=enable
    • EL0PCTEN
      • PL0에서 physical 카운터 및 frequency 레지스터의 접근 설정. 0=disable, 1=enable
  • CNTP_CTL_EL0, Counter-timer Physical Timer Control register
  • CNTV_CTL_EL0, Counter-timer Virtual Timer Control register
    • ISTATUS
      • 타이머 조건 만족 여부. 1=meet condition, 0=not meet condition
    • IMASK
      • 타이머 조건을 만족시키는 경우 인터럽트 발생 여부를 제어한다. 0=assert(타이머 설정 시 사용), 1=none
    • ENABLE
      • 타이머 사용 여부. 1=enable, 0=disable(shutdown)
  • CNTPCT_EL0, Counter-timer Physical Count register
  • CNTVCT_EL0, Counter-timer Virtual Count register
    • bit[61..0]에 카운터 값이 있다.
  • CNTP_TVAL_EL0, Counter-timer Physical Timer TimerValue register
  • CNTV_TVAL_EL0, Counter-timer Virtual Timer TimerValue register
    • bit[31..0]에 타이머 값이 설정된다.
  • CNTHCTL_EL2, Counter-timer Hypervisor Control register
  • CNTHP_CTL_EL2, Counter-timer Hypervisor Physical Timer Control register
  • CNTHP_CVAL_EL2, Counter-timer Physical Timer CompareValue register (EL2)
  • CNTHP_TVAL_EL2, Counter-timer Physical Timer TimerValue register (EL2)
  • CNTHPS_CTL_EL2, Counter-timer Secure Physical Timer Control register (EL2)
  • CNTHPS_CVAL_EL2, Counter-timer Secure Physical Timer CompareValue register (EL2)
  • CNTHPS_TVAL_EL2, Counter-timer Secure Physical Timer TimerValue register (EL2)
  • CNTHV_CTL_EL2, Counter-timer Virtual Timer Control register (EL2)
  • CNTHV_CVAL_EL2, Counter-timer Virtual Timer CompareValue register (EL2)
  • CNTHV_TVAL_EL2, Counter-timer Virtual Timer TimerValue Register (EL2)
  • CNTHVS_CTL_EL2, Counter-timer Secure Virtual Timer Control register (EL2)
  • CNTHVS_CVAL_EL2, Counter-timer Secure Virtual Timer CompareValue register (EL2)
  • CNTHVS_TVAL_EL2, Counter-timer Secure Virtual Timer TimerValue register (EL2)
  • CNTP_CVAL_EL0, Counter-timer Physical Timer CompareValue register
  • CNTPS_CTL_EL1, Counter-timer Physical Secure Timer Control register
  • CNTPS_CVAL_EL1, Counter-timer Physical Secure Timer CompareValue register
  • CNTPS_TVAL_EL1, Counter-timer Physical Secure Timer TimerValue register
  • CNTV_CVAL_EL0, Counter-timer Virtual Timer CompareValue register
  • CNTVOFF_EL2, Counter-timer Virtual Offset register

 

다음 그림은 10ms 타이머를 설정하고 만료 시 타이머 인터럽트가 발생하는 과정을 타이머 레지스터로 보여준다.

 

ARM32용 generic 타이머 레지스터

 

다음은 ARMv7의 타이머 관련 레지스터들을 보여준다.

 

2) BCM2708(rpi) 타이머

32비트 raspberry pi에서 사용된 글로벌 타이머이다. 이외에 몇몇 시스템에서 한정하여 사용된다.

특징

  • 1개의 글로벌 타이머 제공
  • 연속(continuous) 모드만 지원
  • extra 클럭 분주기 레지스터
  • extra stop 디버그 모드 컨트롤 비트
  • 32bit 프리 러닝 카운터
  • ARM 시스템 타이머를 일부 개량하였다.
  • rpi: 1Mhz 클럭을 사용

 

시스템 타이머 레지스터

  • Timer Load
  • Timer Value (RO)
  • Timer Control (RW)
    • 아래 그림 참고
  • Timer IRQ Clear/Ack (WO)
    • 1을 기록 시 인터럽트 pending bit를 클리어한다.
    • 리셋 값: 0x003e_0020
  • Timer RAW IRQ (RO)
    • 인터럽트 pending bit 상태를 알아온다.
      • 0=clear, 1=set
  • Timer Masked IRQ (RO)
    • 인터럽트 마스크 상태를 알아온다.
      • 0=인터럽트 disable(mask), 1=인터럽트 enabled(unmask)
  • Timer Reload (RW)
    • Load 레지스터의 사본과 동일하다 다만 다른 점은 값을 설정해도 리로드가 즉시 발생하지 않는다.
  • Timer Pre-divider (RW)
    • APB 클럭을 설정된 lsb 10비트 값 + 1로 분주한다.
      • timer_clock = apb_clock / (pre_divider + 1)
    • 리셋 시 0x7d가 설정된다.
  • Timer Free Running Counter

 

Timer Control Register

ARM의 SP804는 8비트만 사용하는데 Boradcomm이 이를 확장하여 24bit를 운용한다.

  • 8bit pre-scaler를 사용하여 시스템 클럭 / (prescaler 값 + 1)이 사용된다.
  • 리셋 값: 0x003e_0020
    • pre-scaler: 62 (0x3e)
    • timer interrupt enable (bit5)

 


Device Tree 기반 타이머 초기화

다음 그림은 arm 및 arm64 아키텍처에 내장된 generic 타이머를 사용하는 드라이버의 초기화 함수를 호출하는 예를 보여준다.

 

timer_probe()

drivers/clocksource/timer-probe.c

void __init timer_probe(void)
{
        struct device_node *np;
        const struct of_device_id *match;
        of_init_fn_1_ret init_func_ret;
        unsigned timers = 0;
        int ret;

        for_each_matching_node_and_match(np, __timer_of_table, &match) {
                if (!of_device_is_available(np))
                        continue;

                init_func_ret = match->data;

                ret = init_func_ret(np);
                if (ret) {
                        if (ret != -EPROBE_DEFER)
                                pr_err("Failed to initialize '%pOF': %d\n", np,
                                       ret);
                        continue;
                }

                timers++;
        }

        timers += acpi_probe_device_table(timer);

        if (!timers)
                pr_crit("%s: no matching timers found\n", __func__);
}

커널에 등록된 디바이스 드라이버들 중 디바이스 트리 또는 ACPI 정보가 요청한 타이머를 찾아 초기화(probe) 함수를 호출한다.

  • _timer_of_table 섹션에 등록된 디바이스 드라이버들의 이름과 Device Tree 또는 ACPI 테이블로 부터 읽은 노드의 디바이스명이 매치되는 드라이버들의 초기화 함수들을 호출한다.

 

per core 아키텍처 내장 타이머 초기화 (armv7, armv8)

다음 그림은 arm 및 arm64 아키텍처 내장형 generic 타이머 초기화 함수의 함수 호출 관계를 보여준다.

 

arch_timer_of_init()

drivers/clocksource/arm_arch_timer.c

static int __init arch_timer_of_init(struct device_node *np)
{
        int i, ret;
        u32 rate;

        if (arch_timers_present & ARCH_TIMER_TYPE_CP15) {
                pr_warn("multiple nodes in dt, skipping\n");
                return 0;
        }

        arch_timers_present |= ARCH_TIMER_TYPE_CP15;
        for (i = ARCH_TIMER_PHYS_SECURE_PPI; i < ARCH_TIMER_MAX_TIMER_PPI; i++)
                arch_timer_ppi[i] = irq_of_parse_and_map(np, i);

        arch_timer_populate_kvm_info();

        rate = arch_timer_get_cntfrq();
        arch_timer_of_configure_rate(rate, np);

        arch_timer_c3stop = !of_property_read_bool(np, "always-on");

        /* Check for globally applicable workarounds */
        arch_timer_check_ool_workaround(ate_match_dt, np);

        /*
         * If we cannot rely on firmware initializing the timer registers then
         * we should use the physical timers instead.
         */
        if (IS_ENABLED(CONFIG_ARM) &&
            of_property_read_bool(np, "arm,cpu-registers-not-fw-configured"))
                arch_timer_uses_ppi = ARCH_TIMER_PHYS_SECURE_PPI;
        else
                arch_timer_uses_ppi = arch_timer_select_ppi();

        if (!arch_timer_ppi[arch_timer_uses_ppi]) {
                pr_err("No interrupt available, giving up\n");
                return -EINVAL;
        }

        /* On some systems, the counter stops ticking when in suspend. */
        arch_counter_suspend_stop = of_property_read_bool(np,
                                                         "arm,no-tick-in-suspend");

        ret = arch_timer_register();
        if (ret)
                return ret;

        if (arch_timer_needs_of_probing())
                return 0;

        return arch_timer_common_init();
}
TIMER_OF_DECLARE(armv7_arch_timer, "arm,armv7-timer", arch_timer_of_init);
TIMER_OF_DECLARE(armv8_arch_timer, "arm,armv8-timer", arch_timer_of_init);

armv7 & armv8 아키텍처에 내장된 Generic 타이머를 사용하는 디바이스 트리용 디바이스 드라이버의 초기화 함수이다.

  • 코드 라인 6~11에서 디바이스 트리에서 cp15를 사용하는 방식의 per-core 아키텍처 내장형 타이머가 이 드라이버를 사용하여 초기화 함수로 진입하는 경우 한 번만 초기화하고 그 이후 호출되는 경우 skip 한다.
    • 참고로 cp15를 사용하는 arm per-core 아키텍처 내장형 타이머와 arm 메모리 맵드 아키텍처 내장 타이머 두 개 다 같이 운영될 수 있다.
  • 코드 라인 12~13에서 아키텍처의 각 코어가 지원하는 최대 4개 타이머 수만큼 순회하며 타이머 노드의 “intrrupts” 속성 값을 읽어 각 타이머가 연결된 PPI에 해당하는 인터럽트를 요청하고 그 결과인 virq를 저장한다.
  • 코드 라인 15에서 커널의 타이머 virq와 Guest 커널용 타이머 virq를 각각 kvm 타이머 정보에 저장한다.
  • 코드 라인 17~18에서 다음 두 가지 방법으로 클럭 주파수 값을 알아와서 arch_timer_rate에 저장한다.
    • 디바이스 트리를 사용하는 경우
      • “clock-frequency” 속성 값에서 클럭 주파수 값을 알아온다. 만일 속성이 지정되지 않은 경우 CNTFRQ(Counter Frequency Register) 레지스터에서 읽은 값을 클럭 주파수 값으로 사용한다.
    • ACPI를 사용하는 경우
      • CNTFRQ 레지스터에서 읽은 값을 클럭 주파수 값으로 사용한다.
  • 코드 라인 20에서 절전(c3stop) 기능 여부를 알아온다.
    • 타이머 노드의 “always-on” 속성이 없는 경우 절전 기능이 사용되도록 arch_timer_c3stop을 true로 설정한다.
    • deep-sleep 상태의 코어에 대해 타이머 장치나 인터럽트 장치도 절전을 위해 파워가 off될 수 있는데 이러한 절전(c3stop) 기능이 사용되면 안되는 시스템에서는 “always-on”을 설정한다.
  • 코드 라인 23에서 Cortex-A72 타이머 errta 루틴을 동작시킨다.
    • unstable한 코어를 찾으면 유저 스페이스 인터페이스(vdso-direct 포함)에서 직접 타이머 레지스터의 액세스를 사용하지 않게 한다.
  • 코드 라인 29~38에서 시스템이 지원하는 최대 4개의 타이머들 중 현재 커널이 사용할 적절한 타이머를 선택한다.
    • 타이머 노드의 “arm,cpu-registers-not-fw-configured” 속성이 있는 경우 시큐어 펌웨어를 사용하지 않는 경우이다. 이러한 경우 시큐어용 타이머를 커널에서 사용하도록 선택한다. 이 옵션을 사용하는 장점으로 남는 두 타이머(non-secure phys 타이머 및 virt 타이머)들을 guest os를 위해 더 남겨둘 수 있다.
  • 코드 라인 41~42에서 “arm,no-tick-in-suspend” 속성이 존재하는 경우 타이머가 suspend 기능이 있다고 판단하도록 arch_counter_suspend_stop을 true로 설정한다.
  • 코드 라인 44~46에서 현재 커널이 사용하도록 지정된 Generic 타이머를 per-cpu 인터럽트에 등록하고 boot cpu용은 즉시 enable하고 클럭 이벤트 디바이스에 등록한다
  • 코드 라인 48~51에서 cp15를 사용하는 arm per-core 아키텍처 내장형 타이머와 arm 메모리 맵드 아키텍처 내장 타이머 두 개 다 같이 운영해야 하는 경우 둘 다 probe 된 경우에만 마지막에 arch_timer_common_init() 함수가 호출되도록 한다. 이 루틴에서는 가장 높은 rate의 Generic 타이머를 클럭 소스 및 스케줄러 클럭으로 등록한다.

 

다음 그림은 디바이스 트리의 타이머 노드 정보를 읽어오는 모습을 보여준다. (예: rpi3 & rpi4)

 

arch_timer_populate_kvm_info()

drivers/clocksource/arm_arch_timer.c

static void __init arch_timer_populate_kvm_info(void)
{
        arch_timer_kvm_info.virtual_irq = arch_timer_ppi[ARCH_TIMER_VIRT_PPI];
        if (is_kernel_in_hyp_mode())
                arch_timer_kvm_info.physical_irq = arch_timer_ppi[ARCH_TIMER_PHYS_NONSECURE_PPI];
}

kvm을 위해 사용할 타이머 정보를 저장한다.

  • 코드 라인 3에서 guest os용 타이머 irq를 kvm용 가상 타이머 정보에 저장한다.
  • 코드 라인 4~5에서 커널이 하이퍼모드에서 동작한 경우 non-secure 물리 타이머용 irq를 kvm용 물리 타이머 정보에 저장한다.

 

타이머 Rate 읽기

arch_timer_get_cntfrq() – ARM64

arch/arm64/include/asm/arch_timer.h

static inline u32 arch_timer_get_cntfrq(void)
{
        return read_sysreg(cntfrq_el0);
}

CNTFRQ_EL0(Counter Frequency Register)로부터 rate 값을 읽어 반환한다.

 

read_sysreg() – ARM64

arch/arm64/include/asm/sysreg.h

/*
 * Unlike read_cpuid, calls to read_sysreg are never expected to be
 * optimized away or replaced with synthetic values.
 */
#define read_sysreg(r) ({                                       \
        u64 __val;                                              \
        asm volatile("mrs %0, " __stringify(r) : "=r" (__val)); \
        __val;                                                  \
})

@r 레지스터 값을 읽어온다. (u64 값 반환)

 

arch_timer_get_cntfrq() – ARM32

arch/arm/include/asm/arch_timer.h

static inline u32 arch_timer_get_cntfrq(void)
{
        u32 val;
        asm volatile("mrc p15, 0, %0, c14, c0, 0" : "=r" (val));
        return val;
}

CNTFRQ(Counter Frequency Register)로부터 rate 값을 읽어 반환한다.

  • CNTFRQ(Counter Frequency Register)
    • 시스템 카운터의 주파수가 담겨있다. 32bit 값을 사용한다.
    • rpi2: 19,200,000 (19.2Mhz)

 

arch_timer_of_configure_rate()

drivers/clocksource/arm_arch_timer.c

/*
 * For historical reasons, when probing with DT we use whichever (non-zero)
 * rate was probed first, and don't verify that others match. If the first node
 * probed has a clock-frequency property, this overrides the HW register.
 */
static void arch_timer_of_configure_rate(u32 rate, struct device_node *np)
{
        /* Who has more than one independent system counter? */
        if (arch_timer_rate)
                return;

        if (of_property_read_u32(np, "clock-frequency", &arch_timer_rate))
                arch_timer_rate = rate;

        /* Check the timer frequency. */
        if (arch_timer_rate == 0)
                pr_warn("frequency not available\n");
}

디바이스 트리의 @np 타이머 노드에서 “clock-frequency” 속성을 읽어 아키텍처 내장형 타이머의 rate 값을 저장한다.

  • 코드 라인 4~5에서 아키텍처 내장형 타이머의 rate 값이 이미 설정된 경우 함수를 빠져나간다.
  • 코드 라인 7~8에서 디바이스 트리의 @np 타이머 노드에서 “clock-frequency” 속성을 읽어 아키텍처 내장형 타이머의 rate 값을 저장한다.
  • 코드 라인 11~12에서 rate 값이 여전히 0이면 경고 메시지를 출력한다.

 

적절한 타이머 PPI 선택

arch_timer_select_ppi()

drivers/clocksource/arm_arch_timer.c

/**
 * arch_timer_select_ppi() - Select suitable PPI for the current system.
 *
 * If HYP mode is available, we know that the physical timer
 * has been configured to be accessible from PL1. Use it, so
 * that a guest can use the virtual timer instead.
 *
 * On ARMv8.1 with VH extensions, the kernel runs in HYP. VHE
 * accesses to CNTP_*_EL1 registers are silently redirected to
 * their CNTHP_*_EL2 counterparts, and use a different PPI
 * number.
 *
 * If no interrupt provided for virtual timer, we'll have to
 * stick to the physical timer. It'd better be accessible...
 * For arm64 we never use the secure interrupt.
 *
 * Return: a suitable PPI type for the current system.
 */
static enum arch_timer_ppi_nr __init arch_timer_select_ppi(void)
{
        if (is_kernel_in_hyp_mode())
                return ARCH_TIMER_HYP_PPI;

        if (!is_hyp_mode_available() && arch_timer_ppi[ARCH_TIMER_VIRT_PPI])
                return ARCH_TIMER_VIRT_PPI;

        if (IS_ENABLED(CONFIG_ARM64))
                return ARCH_TIMER_PHYS_NONSECURE_PPI;

        return ARCH_TIMER_PHYS_SECURE_PPI;
}

현재 커널 시스템이 사용할 적절한 타이머 PPI를 선택한다.

  • 코드 라인 3~4에서 커널이 하이퍼 모드에서 동작하는 경우 하이퍼 바이저용 타이머 PPI를 선택한다.
  • 코드 라인 6~7에서 시스템이 제공하지 않거나 다른 상위에서 하이퍼 바이저가 동작 중이라, 하이퍼 모드를 사용할 수 없는 경우 이 커널은 guest os용 타이머 PPI를 선택한다.
  • 코드 라인 9~12에서 arm64 커널인 경우 non-secure 커널 타이머 PPI를 선택하고, arm32 커널인 경우 secure 커널 타이머를 선택한다.

 

arch_timer_needs_of_probing()

drivers/clocksource/arm_arch_timer.c

static bool __init arch_timer_needs_of_probing(void)
{
        struct device_node *dn;
        bool needs_probing = false;
        unsigned int mask = ARCH_TIMER_TYPE_CP15 | ARCH_TIMER_TYPE_MEM;

        /* We have two timers, and both device-tree nodes are probed. */
        if ((arch_timers_present & mask) == mask)
                return false;

        /*
         * Only one type of timer is probed,
         * check if we have another type of timer node in device-tree.
         */
        if (arch_timers_present & ARCH_TIMER_TYPE_CP15)
                dn = of_find_matching_node(NULL, arch_timer_mem_of_match);
        else
                dn = of_find_matching_node(NULL, arch_timer_of_match);

        if (dn && of_device_is_available(dn))
                needs_probing = true;

        of_node_put(dn);

        return needs_probing;
}

cp15 및 mmio 두 타입의 아키텍처 내장형 타이머가 둘 다 이미 probe 되었는지 여부를 반환한다.

  • 코드 라인 5~9에서 cp15 및 mmio 타입 둘 다 probing 완료되었으면 더 이상 probing이 필요 없으므로 false를 반환한다.
  • 코드 라인 15~16에서 cp15 타이머가 이미 probing된 경우 mmio 타입 타이머가 매치되는지 확인한다.
  • 코드 라인 17~18에서 mmio 타이머가 이미 probing된 경우 cp15 타입 타이머가 매치되는지 확인한다.
  • 코드 라인 20~25에서 디바이스가 사용 가능한 상태라면 probing을 하기 위해 true를 반환한다. 그렇지 않은 경우 false를 반환한다.

 

arch_timer_common_init()

drivers/clocksource/arm_arch_timer.c

static int __init arch_timer_common_init(void)
{
        arch_timer_banner(arch_timers_present);
        arch_counter_register(arch_timers_present);
        return arch_timer_arch_init();
}

generic 타이머를 클럭 소스로 등록하고 스케줄러 클럭과 딜레이 타이머로 등록한다.

  • 코드 3에서 generic 타이머 정보를 출력한다. cp15 타이머인지 mmio 타이머인지 여부를 가려낼 수 있다.
    • arm64-rpi4: “arch_timer: cp15 timer(s) running at 54.00MHz (phys).”를 출력한다.
    • arm32 rpi2: “Architected cp15 timer(s) running at 19.20MHz (virt).”를 출력한다.
    • arm32 rpi3: “Architected cp15 timer(s) running at 19.20MHz (phys).”를 출력한다.
  • 코드 4에서 generic 타이머를 클럭 소스, timercounter 및 스케쥴 클럭으로 등록한다.
  • 코드 5에서 아키텍처별 타이머 초기화 루틴을 수행한다.
    • arm32에서 generic 타이머를 딜레이 루프 타이머로 등록한다.
    • arm64에서 별도의 초기화 코드가 없다.

 


타이머 제어

타이머 프로그램

4가지 타입 타이머를 프로그램하는 함수이다.

  • arch_timer_set_next_event_virt()
    • 이 함수만 소스를 아래에 보여준다. 나머지는 유사 동등.
    • ARCH_TIMER_VIRT_ACCESS 레지스터를 사용한다.
  • arch_timer_set_next_event_phys()
    • ARCH_TIMER_PHYS_ACCESS 레지스터를 사용한다.
  • arch_timer_set_next_event_virt_mem()
    • ARCH_TIMER_MEM_VIRT_ACCESS 레지스터를 사용한다.
  • arch_timer_set_next_event_phys_mem()
    • ARCH_TIMER_MEM_PHYS_ACCESS 레지스터를 사용한다.

 

arch_timer_set_next_event_virt()

drivers/clocksource/arm_arch_timer.c

static int arch_timer_set_next_event_virt(unsigned long evt,
                                          struct clock_event_device *clk)
{
        set_next_event(ARCH_TIMER_VIRT_ACCESS, evt, clk);
        return 0;
}

virt 타이머를 프로그램한다.

 

set_next_event()

drivers/clocksource/arm_arch_timer.c

static __always_inline void set_next_event(const int access, unsigned long evt,
                                           struct clock_event_device *clk)
{
        unsigned long ctrl;
        ctrl = arch_timer_reg_read(access, ARCH_TIMER_REG_CTRL, clk);
        ctrl |= ARCH_TIMER_CTRL_ENABLE;
        ctrl &= ~ARCH_TIMER_CTRL_IT_MASK;
        arch_timer_reg_write(access, ARCH_TIMER_REG_TVAL, evt, clk);
        arch_timer_reg_write(access, ARCH_TIMER_REG_CTRL, ctrl, clk);
}

@access 타입 타이머 액세스 컨트롤 레지스터를 읽어 타이머를 프로그램한다. 이 때 타이머 레지스터도 @evt 값을 기록한다.

 

타이머 shutdown

4가지 타입 타이머를 shutdown 하는 함수이다.

  • arch_timer_shutdown_virt()
    • 이 함수만 소스를 아래에 보여준다. 나머지는 유사 동등.
    • ARCH_TIMER_VIRT_ACCESS  레지스터를 사용한다.
  • arch_timer_shutdown_phys()
    • ARCH_TIMER_PHYS_ACCESS 레지스터를 사용한다.
  • arch_timer_shutdown_virt_mem()
    • ARCH_TIMER_MEM_VIRT_ACCESS 레지스터를 사용한다.
  • arch_timer_shutdown_phys_mem()
    • ARCH_TIMER_MEM_PHYS_ACCESS 레지스터를 사용한다.

 

arch_timer_shutdown_virt()

drivers/clocksource/arm_arch_timer.c

static int arch_timer_shutdown_virt(struct clock_event_device *clk)
{
        return timer_shutdown(ARCH_TIMER_VIRT_ACCESS, clk);
}

virt 타이머 기능을 정지시킨다.

 

timer_shutdown()

drivers/clocksource/arm_arch_timer.c

static __always_inline int timer_shutdown(const int access,
                                          struct clock_event_device *clk)
{
        unsigned long ctrl;

        ctrl = arch_timer_reg_read(access, ARCH_TIMER_REG_CTRL, clk);
        ctrl &= ~ARCH_TIMER_CTRL_ENABLE;
        arch_timer_reg_write(access, ARCH_TIMER_REG_CTRL, ctrl, clk);

        return 0;
}

@access 타입 타이머 컨트롤 레지스터를 읽어 bit0만 클리어 한 후 다시 기록하여 타이머 기능을 정지시킨다.

 


클럭 소스, Timecounter 및 스케줄 클럭으로 등록

arch_counter_register()

drivers/clocksource/arm_arch_timer.c

static void __init arch_counter_register(unsigned type)
{
        u64 start_count;

        /* Register the CP15 based counter if we have one */
        if (type & ARCH_TIMER_TYPE_CP15) {
                u64 (*rd)(void);

                if ((IS_ENABLED(CONFIG_ARM64) && !is_hyp_mode_available()) ||
                    arch_timer_uses_ppi == ARCH_TIMER_VIRT_PPI) {
                        if (arch_timer_counter_has_wa())
                                rd = arch_counter_get_cntvct_stable;
                        else
                                rd = arch_counter_get_cntvct;
                } else {
                        if (arch_timer_counter_has_wa())
                                rd = arch_counter_get_cntpct_stable;
                        else
                                rd = arch_counter_get_cntpct;
                }

                arch_timer_read_counter = rd;
                clocksource_counter.archdata.vdso_direct = vdso_default;
        } else {
                arch_timer_read_counter = arch_counter_get_cntvct_mem;
        }

        if (!arch_counter_suspend_stop)
                clocksource_counter.flags |= CLOCK_SOURCE_SUSPEND_NONSTOP;
        start_count = arch_timer_read_counter();
        clocksource_register_hz(&clocksource_counter, arch_timer_rate);
        cyclecounter.mult = clocksource_counter.mult;
        cyclecounter.shift = clocksource_counter.shift;
        timecounter_init(&arch_timer_kvm_info.timecounter,
                         &cyclecounter, start_count);

        /* 56 bits minimum, so we assume worst case rollover */
        sched_clock_register(arch_timer_read_counter, 56, arch_timer_rate);
}

generic 타이머를 클럭 소스로 등록하고 스케줄러 클럭으로도 등록한다. 단 cp15 및 mmio generic 두 타이머가 사용가능하면 cp15를 우선 사용한다.

  • 코드 라인 6~23에서 가능하면 cp15 기반의 타이머를 우선 사용하도록 전역 arch_timer_read_counter 함수에 virtual 또는 physical 타이머용 함수를 대입한다.
    • 커널에 CONFIG_ARM_ARCH_TIMER_OOL_WORKAROUND 적용된 경우 stable용 함수를 사용한다.
  • 코드 라인 24~26에서 메모리 mapped(mmio) 방식의 타이머를 사용하는 경우 전역 arch_timer_read_counter 함수에 메모리 mapped 방식의 virtual 타이머를 읽어내는 함수를 대입한다.
  • 코드 라인 28~29에서 “arm,no-tick-in-suspend” 속성이 없는 일반적인 시스템인 경우 CLOCK_SOURCE_SUSPEND_NONSTOP 플래그를 추가한다.
  • 코드 라인 30에서 타이머 값을 읽어 시작 카운터 값으로 사용한다.
  • 코드 라인 31에서 “arch_sys_counter”라는 이름의 56비트 클럭소스카운터를 클럭 소스로 등록한다.
  • 코드 라인 32~35에서 56비트 타임카운터/사이클 카운터를 초기화한다.
    • rpi2: 아키텍처 Generic 타이머를 사용하여 타임카운터를 초기화한다.
      • mult=0x3415_5555, shift=24로 설정하여 19.2Mhz 클럭 카운터로 1 cycle 당 52ns가 소요되는 것을 알 수 있다.
      • cyclecounter_cyc2ns() 함수를 호출하여 변동된 cycle 카운터 값으로 소요 시간 ns를 산출한다.
  • 코드 라인 38에서 56비트 카운터를 사용하여 스케줄러 클럭으로 등록한다.

 

다음 그림은 arch_counter_register() 함수의 처리 과정을  보여준다.

 

사이클 카운터 값 읽기

arch_counter_read_cc()

drivers/clocksource/arm_arch_timer.c

static cycle_t arch_counter_read_cc(const struct cyclecounter *cc)
{
        return arch_timer_read_counter();
}

커널에서 사용하는 사이클 카운터 레지스터 값을 읽어온다.

 

/*                    
 * Default to cp15 based access because arm64 uses this function for
 * sched_clock() before DT is probed and the cp15 method is guaranteed
 * to exist on arm64. arm doesn't use this before DT is probed so even
 * if we don't have the cp15 accessors we won't have a problem.
 */     
u64 (*arch_timer_read_counter)(void) = arch_counter_get_cntvct;

사이클 카운터 레지스터 값을 읽어오는 함수가 대입되며, 초기 값은 virtual 카운터를 읽어오는 함수가 지정된다.

 

arch_counter_get_cntvct()

arch/arm/include/asm/arch_timer.h

static inline u64 arch_counter_get_cntvct(void)
{
        u64 cval;

        isb();
        asm volatile("mrrc p15, 1, %Q0, %R0, c14" : "=r" (cval));
        return cval;
}

CNTVCT(64bit Virtual Counter Register)을 읽어 반환한다.

 


Delay 타이머로 등록(for arm32)

arch_timer_arch_init()

arch/arm/kernel/arch_timer.c

int __init arch_timer_arch_init(void)
{                       
        u32 arch_timer_rate = arch_timer_get_rate();

        if (arch_timer_rate == 0)
                return -ENXIO;
                
        arch_timer_delay_timer_register();

        return 0;
}

아키텍처 generic 타이머를 딜레이 루프 타이머로 등록한다.

 

arch_timer_delay_timer_register()

arch/arm/kernel/arch_timer.c

static void __init arch_timer_delay_timer_register(void)
{                       
        /* Use the architected timer for the delay loop. */
        arch_delay_timer.read_current_timer = arch_timer_read_counter_long;
        arch_delay_timer.freq = arch_timer_get_rate();
        register_current_timer_delay(&arch_delay_timer);
}

아키텍처 generic 타이머를 딜레이 루프 타이머로 등록하고 초기화한다.

 


타이머(clock events) 등록

arch_timer_register()

drivers/clocksource/arm_arch_timer.c

static int __init arch_timer_register(void)
{
        int err;
        int ppi;

        arch_timer_evt = alloc_percpu(struct clock_event_device);
        if (!arch_timer_evt) {
                err = -ENOMEM;
                goto out;
        }

        ppi = arch_timer_ppi[arch_timer_uses_ppi];
        switch (arch_timer_uses_ppi) {
        case ARCH_TIMER_VIRT_PPI:
                err = request_percpu_irq(ppi, arch_timer_handler_virt,
                                         "arch_timer", arch_timer_evt);
                break;
        case ARCH_TIMER_PHYS_SECURE_PPI:
        case ARCH_TIMER_PHYS_NONSECURE_PPI:
                err = request_percpu_irq(ppi, arch_timer_handler_phys,
                                         "arch_timer", arch_timer_evt);
                if (!err && arch_timer_has_nonsecure_ppi()) {
                        ppi = arch_timer_ppi[ARCH_TIMER_PHYS_NONSECURE_PPI];
                        err = request_percpu_irq(ppi, arch_timer_handler_phys,
                                                 "arch_timer", arch_timer_evt);
                        if (err)
                                free_percpu_irq(arch_timer_ppi[ARCH_TIMER_PHYS_SECURE_PPI],
                                                arch_timer_evt);
                }
                break;
        case ARCH_TIMER_HYP_PPI:
                err = request_percpu_irq(ppi, arch_timer_handler_phys,
                                         "arch_timer", arch_timer_evt);
                break;
        default:
                BUG();
        }

        if (err) {
                pr_err("can't register interrupt %d (%d)\n", ppi, err);
                goto out_free;
        }

        err = arch_timer_cpu_pm_init();
        if (err)
                goto out_unreg_notify;

        /* Register and immediately configure the timer on the boot CPU */
        err = cpuhp_setup_state(CPUHP_AP_ARM_ARCH_TIMER_STARTING,
                                "clockevents/arm/arch_timer:starting",
                                arch_timer_starting_cpu, arch_timer_dying_cpu);
        if (err)
                goto out_unreg_cpupm;
        return 0;

out_unreg_cpupm:
        arch_timer_cpu_pm_deinit();

out_unreg_notify:
        free_percpu_irq(arch_timer_ppi[arch_timer_uses_ppi], arch_timer_evt);
        if (arch_timer_has_nonsecure_ppi())
                free_percpu_irq(arch_timer_ppi[ARCH_TIMER_PHYS_NONSECURE_PPI],
                                arch_timer_evt);

out_free:
        free_percpu(arch_timer_evt);
out:
        return err;
}

ARMv7 & ARMv8 아키텍처에 내장된 Generic 타이머를 클럭 이벤트 디바이스로 per-cpu 인터럽트에 등록하고 boot cpu용은 즉시 enable한다.

  • 코드 라인 6~10에서 clock_event_device 구조체를 할당받아 온다. 메모리 할당이 실패하는 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 12에서 아키텍처 내장 generic 타이머에 사용될 인터럽트 ppi를 알아온다.
  • 코드 라인 13~42에서 ppi에 해당하는 인터럽트를 요청하고, 핸들러를 설정한다.
  • 코드 라인 44~46에서 CONFIG_CPU_PM 커널 옵션을 사용하는 경우 cpu 절전 상태 변화에 따라 호출되도록 cpu pm notify chain에 arch_timer_cpu_pm_notify() 함수를 등록한다.
  • 코드 라인 49~54에서 cpu 상태 변화에 따라 호출되도록 cpu notify chain에 arch_timer_cpu_notify() 함수를 등록하고, boot cpu에 대한 cp15용 generic 타이머 설정 및 per-cpu 인터럽트를 enable 한 후 정상 값 0을 반환한다.
    • secondary cpu가 on될 때마다 타이머가 초기화되고 off될 때마다 migration된다.

 

다음 그림은 클럭 이벤트 디바이스를 통해 per-cpu 인터럽트 핸들러가 등록되는 것을 보여준다.

 

arch_timer_starting_cpu()

drivers/clocksource/arm_arch_timer.c

static int arch_timer_starting_cpu(unsigned int cpu)
{
        struct clock_event_device *clk = this_cpu_ptr(arch_timer_evt);
        u32 flags;

        __arch_timer_setup(ARCH_TIMER_TYPE_CP15, clk);

        flags = check_ppi_trigger(arch_timer_ppi[arch_timer_uses_ppi]);
        enable_percpu_irq(arch_timer_ppi[arch_timer_uses_ppi], flags);

        if (arch_timer_has_nonsecure_ppi()) {
                flags = check_ppi_trigger(arch_timer_ppi[ARCH_TIMER_PHYS_NONSECURE_PPI]);
                enable_percpu_irq(arch_timer_ppi[ARCH_TIMER_PHYS_NONSECURE_PPI],
                                  flags);
        }

        arch_counter_set_user_access();
        if (evtstrm_enable)
                arch_timer_configure_evtstream();

        return 0;
}

요청한 cpu의 per-core 아키텍처 내장형 generic 타이머를 가동한다.

  • 코드 라인 6에서 per-cpu 클럭 이벤트 디바이스에 대한 cp15용 generic 타이머 설정을 한다. (초기 shutdown)
  • 코드 라인 8~9에서 사용하는 타이머의 per-cpu 인터럽트를 enable한다.
  • 코드 라인 11~15에서 nonsecure ppi인 경우 nonsecure용 물리 타이머의 per-cpu 인터럽트를 enable한다.
  • 코드 라인 17에서 타이머의 user access를 허용한다
    • CNTKCTL(Timer PL1 Control Register).PL0VCTEN 비트를 설정하여 Virtual 타이머의 PL0 access를 허용하게 한다.
  • 코드 라인 18~19에서 시스템에 따라 이벤트 스트림(10khz)도 구성한다.

 

__arch_timer_setup()

drivers/clocksource/arm_arch_timer.c

static void __arch_timer_setup(unsigned type,
                               struct clock_event_device *clk)
{
        clk->features = CLOCK_EVT_FEAT_ONESHOT;

        if (type == ARCH_TIMER_TYPE_CP15) {
                typeof(clk->set_next_event) sne;

                arch_timer_check_ool_workaround(ate_match_local_cap_id, NULL);

                if (arch_timer_c3stop)
                        clk->features |= CLOCK_EVT_FEAT_C3STOP;
                clk->name = "arch_sys_timer";
                clk->rating = 450;
                clk->cpumask = cpumask_of(smp_processor_id());
                clk->irq = arch_timer_ppi[arch_timer_uses_ppi];
                switch (arch_timer_uses_ppi) {
                case ARCH_TIMER_VIRT_PPI:
                        clk->set_state_shutdown = arch_timer_shutdown_virt;
                        clk->set_state_oneshot_stopped = arch_timer_shutdown_virt;
                        sne = erratum_handler(set_next_event_virt);
                        break;
                case ARCH_TIMER_PHYS_SECURE_PPI:
                case ARCH_TIMER_PHYS_NONSECURE_PPI:
                case ARCH_TIMER_HYP_PPI:
                        clk->set_state_shutdown = arch_timer_shutdown_phys;
                        clk->set_state_oneshot_stopped = arch_timer_shutdown_phys;
                        sne = erratum_handler(set_next_event_phys);
                        break;
                default:
                        BUG();
                }

                clk->set_next_event = sne;
        } else {
                clk->features |= CLOCK_EVT_FEAT_DYNIRQ;
                clk->name = "arch_mem_timer";
                clk->rating = 400;
                clk->cpumask = cpu_possible_mask;
                if (arch_timer_mem_use_virtual) {
                        clk->set_state_shutdown = arch_timer_shutdown_virt_mem;
                        clk->set_state_oneshot_stopped = arch_timer_shutdown_virt_mem;
                        clk->set_next_event =
                                arch_timer_set_next_event_virt_mem;
                } else {
                        clk->set_state_shutdown = arch_timer_shutdown_phys_mem;
                        clk->set_state_oneshot_stopped = arch_timer_shutdown_phys_mem;
                        clk->set_next_event =
                                arch_timer_set_next_event_phys_mem;
                }
        }

        clk->set_state_shutdown(clk);

        clockevents_config_and_register(clk, arch_timer_rate, 0xf, 0x7fffffff);
}

아키텍처에 내장된 generic 타이머를 클럭 이벤트 디바이스로 설정하여 per-cpu 인터럽트와 핸들러를 설정하고 타이머는 shutdown 한다.

  • 코드 라인 4에서 generic 타이머에 oneshot 기능을 부여한다.
  • 코드 라인 6~12에서 보조프로세서 cp15로 제어되는 타이머인 경우이면서 c3stop 기능이 있는 경우 features에 C3STOP 플래그를 추가한다.
    • rpi2: “Always-on” 속성을 사용하여 c3stop을 사용하지 않는다.
  • 코드 라인 13~34에서 “arch_sys_timer”라는 이름으로 현재 cpu에 대해서만 cpumask를 설정하고 rating을 450으로 설정한다. 그리고 ppi 타입에 해당하는 타이머 인터럽트 및 핸들러를 설정한다.
  • 코드 라인 35~51에서 메모리 mapped 타이머를 지원하는 경우 feauters에 DYNIRQ 플래그를 추가하고 “arch_mem_timer”라는 이름으로 전체 cpu에 대해서 cpumask를 설정하고 rating을 400으로 설정한다.그리고 virtual 또는 physical 두 타입 중 하나의 타이머 인터럽트 및 핸들러를 설정한다.
  • 코드 라인 53에서 현재 cpu의 타이머 출력을 정지시킨다.
  • 코드 라인 55에서 클럭 이벤트 디바이스 설정 및 등록을 진행한다.

 

다음 그림은 __arch_timer_setup() 함수의 처리 과정을 보여준다.

 


이벤트 스트림 (for ARM)

ARM 아키텍처에서 delay() API의 내부 polling을 최소화하고, 저전력으로 구동하기 위해 wfe 명령을 사용한 구현에서 일정 스트림 간격으로 cpu를 깨우기 위해 사용된다.

 

이벤트 스트림 설정 및 가동

arch_timer_configure_evtstream()

drivers/clocksource/arm_arch_timer.c

static void arch_timer_configure_evtstream(void)
{
        int evt_stream_div, pos;

        /* Find the closest power of two to the divisor */
        evt_stream_div = arch_timer_rate / ARCH_TIMER_EVT_STREAM_FREQ;
        pos = fls(evt_stream_div);
        if (pos > 1 && !(evt_stream_div & (1 << (pos - 2))))
                pos--;
        /* enable event stream */
        arch_timer_evtstrm_enable(min(pos, 15));
}

아키텍처 generic 타이머 중 virtual 타이머에 이벤트 스트림을 위해 약 10Khz에 해당하는 트리거를 설정하고 동작시킨다.

  • 코드 라인 6에서 divider 값으로 사용할 값을 구한다.
    • 19.2Mhz / 10Khz = 1920
  • 코드 라인 7~9에서 divider 값을 2의 차수 단위로 정렬한 값에 해당하는 비트 수 pos를 구한다.
    • pos = 11 (2 ^ 11 = 2048 분주)
  • 코드 라인 11에서 virtual 타이머에 이벤트 스트림의 분주율에 사용할 pos 값으로 설정하고 이벤트 스트림 기능을 enable한다.

 

다음 그림은 54Mhz의 클럭 소스를 4096 분주하여 목표치 10Khz에 가장 근접한 13.183Khz 주기의 이벤트 스트림용 트리거를 enable하는 것을 보여준다.

 

다음 그림은 19.2Mhz의 클럭 소스를 2048 분주하여 목표치 10Khz에 가장 근접한 9.375Khz 주기의 이벤트 스트림용 트리거를 enable하는 것을 보여준다.

 

arch_timer_evtstrm_enable()

drivers/clocksource/arm_arch_timer.c

static void arch_timer_evtstrm_enable(int divider)
{
        u32 cntkctl = arch_timer_get_cntkctl();

        cntkctl &= ~ARCH_TIMER_EVT_TRIGGER_MASK;
        /* Set the divider and enable virtual event stream */
        cntkctl |= (divider << ARCH_TIMER_EVT_TRIGGER_SHIFT)
                        | ARCH_TIMER_VIRT_EVT_EN;
        arch_timer_set_cntkctl(cntkctl);
        arch_timer_set_evtstrm_feature();
        cpumask_set_cpu(smp_processor_id(), &evtstrm_available);
}

Generic 타이머의 이벤트 스트림 기능을 enable하기 위해 CNTKCTL 레지스터에 divider(0~15) 값과 enable 비트를 설정한다.

  • 코드 라인 3에서 CNTKCTL 레지스터 값을 읽어온다.
  • 코드 라인 5에서 CNTKCTL.EVNTI(bit7:4)에 해당하는 비트를 클리어한다.
  • 코드 라인 7~9에서 divider 값과 enable 비트를 추가하고 CNTKCTL 레지스터에 저장한다.
  • 코드 라인 10에서 전역 elf_hwcap에 이벤트 스트림에 대한 플래그들을 추가한다.
  • 코드 라인 11에서 evestrm_available 비트마스크에 현재 cpu를 설정한다.

 

arch_timer_get_cntkctl() – ARM64

arch/arm64/include/asm/arch_timer.h

static inline u32 arch_timer_get_cntkctl(void)
{
        return read_sysreg(cntkctl_el1);
}

CNTKCTL(Timer Control for EL0) 레지스터 값을 읽어온다.

 

arch_timer_get_cntkctl() – ARM32

arch/arm/include/asm/arch_timer.h

static inline u32 arch_timer_get_cntkctl(void)
{
        u32 cntkctl;
        asm volatile("mrc p15, 0, %0, c14, c1, 0" : "=r" (cntkctl));
        return cntkctl;
}

CNTKCTL(Timer PL1 Control) 레지스터 값을 읽어온다.

 

참고

 

Compound 페이지

<kernel v5.0>

Compound 페이지

  • 버디 시스템에서 order 단위로 free 페이지를 관리하고, 할당 받아 사용하는 페이지들은 order를 제거한 상태에서 사용한다. 그런데 order 단위로 묶어 관리하는 페이지가 있는데 이들을 compound 페이지라고 한다.
  • 용도
    • 슬랩 캐시
    • Huge 페이지
      • 일반적인 high order 페이지와 다른 점은 TLB 성능을 극대화하기 위해 설계되었다.
      • 성능을 향상시키기 위해 리눅스는 대량의 페이지(high order 페이지)를 할당 받는 경우 PMD 레벨에서 huge 페이지를 사용하여 더 빠른 access를 사용할 수 있게 매핑을 한다.
      • HugeTLBFS 및 THP(Transparent Huge Page)에서 사용한다.
  • 2015년 12월 kernel v.4.6-rc1 에서 CONFIG_PAGEFLAGS_EXTENDED 옵션과 PG_compound, PG_tail 이 없어지고 PG_head 만 남겨지게 되었다.

 

prep_compound_page()

/mm/page_alloc.c

void prep_compound_page(struct page *page, unsigned long order)
{
        int i;
        int nr_pages = 1 << order;

        set_compound_page_dtor(page, COMPOUND_PAGE_DTOR);
        set_compound_order(page, order);
        __SetPageHead(page);
        for (i = 1; i < nr_pages; i++) {
                struct page *p = page + i;
                set_page_count(p, 0);
                p->mapping = TAIL_MAPPING;
                set_compound_head(p, page);
        }
}

compound 페이지를 준비한다.

  • 코드 라인 6에서 compound 파괴자 id에 COMPOUND_PAGE_DTOR를 대입한다.
    • 파괴자 id는 다음과 같다.
      • NULL_COMPOUND_DTOR
      • COMPOUND_PAGE_DTOR
      • HUGETLB_PAGE_DTOR
      • TRANSHUGE_PAGE_DTOR
  • 코드 라인 7에서 두 번째 페이지에 order를 설정한다.
  • 코드 라인 8에서 헤더 페이지의 플래그에 PG_Head 비트를 설정한다.
  • 코드 라인 9~14에서 나머지 페이지의 참조 카운터에 0을 대입하고, 나머지 페이지들이 헤드 페이지를 가리키게 한 후 플래그에 PG_Tail 비트를 설정한다.

 

다음 그림은 prep_compound_page() 함수에 의해 compound 페이지가 준비되는 모습을 보여준다.

  • 두 번째 페이지에 compound 정보가 존재한다.

 

page_count()

include/linux/mm.h

static inline int page_count(struct page *page)
{
        return atomic_read(&compound_head(page)->_count);
}

요청 페이지의 _count 값을 알아온다. 만일 compound page인 경우 선두 페이지에서 _count 값을 알아온다.

 

compound_order()

include/linux/mm.h

static inline int compound_order(struct page *page)
{
        if (!PageHead(page))
                return 0;
        return page[1].compound_order;
}

compound 페이지이면 compound_order를 알아오고 compound 페이지가 아닌 경우 0 order를 반환한다

  • 첫 번째 페이지의 Head 플래그가 설정된 경우 compound 페이지를 의미한다.
  • 두 번째 페이지 구조체에서 compound_order를 알아온다.

 

다음 그림은 compound 페이지로부터 order 값을 알아오는 과정을 보여준다.

compound_order-1a