Timer -5- (Clock Events Subsytem)

 

Timer -5- (Clock Events Subsytem)

타이머가 인터럽트 컨트롤러에 연결되어 타이머의 만료 시간에 인터럽트 처리를 할 수 있다. 이러한 타이머를 클럭 이벤트 디바이스러 구성하여 커널에 이벤트를 제공할 수 있는 기능을 제공하는 공통 서브시스템이고 다음 두 가지 운용 모드를 사용한다.

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

 

클럭 이벤트 디바이스 기능 타입

  • CLOCK_EVT_FEAT_PERIODIC(0x000001)
    • shutdown 으로 제어하지 않는 한 규칙적으로 이벤트가 발생한다.
  • CLOCK_EVT_FEAT_ONESHOT(0x000002)
    • 타이머 장치가 인터럽트 컨트롤러에 연결되어 단발 이벤트 프로그램이 가능하다.
  • CLOCK_EVT_FEAT_KTIME(0x000004)
    • 아래 CLOCK_EVT_FEAT_HRTIMER 기능과 같이 사용된다.
  • CLOCK_EVT_FEAT_C3STOP(0x00008)
    • cpu가 c3(deep-sleep) 상태에 진입할 때 타이머도 전원이 꺼지는 절전 기능을 가진다.
    • nohz 구현 시 틱 브로드캐스트 디바이스에 의해 wakeup 된다.
  • CLOCK_EVT_FEAT_DUMMY(0x000010)
    • x86 시스템의 Local APIC 에 사용되는 아무것도 수행하지 않는 더미 디바이스 드라이버에서 사용된다.
  • CLOCK_EVT_FEAT_DYNIRQ(0x000020)
    • 브로드캐스트 목적의 인터럽트가 특정 cpu에 고정되지 않고 cpu를 선택(set_irq_affinity)하여 사용할 수 있으며 다음 드라이버에서 사용되고 있다.
      • armv7 또는 armv8 아키텍처에 내장된 generic 타이머의 메모리 mapped 드라이버 – “arm,armv7-timer-mem”
      • “st,nomadik-mtu”
  • CLOCK_EVT_FEAT_PERCPU(0x000040)
    • arm cortex-a9 아키텍처에 긴밀하게 부착된 타이머 장치 타입으로 틱 브로드캐스트 디바이스로 사용되지 않게 제한한다.
    • “arm,cortex-a9-global_timer” 드라이버에서 사용된다.
  • CLOCK_EVT_FEAT_HRTIMER
    • 브로드캐스트에 IPI가 아닌 hrtimer를 사용하여 클럭 이벤트 디바이스로 사용된다.
    • kernel/time/tick-broadcast-hrtimer.c – ONESHOT | KTIME | HRTIMER 기능을 사용하는 것을 볼 수 있다.
    • 참고: tick: Introduce hrtimer based broadcast

 

 

ARM 아키텍처 내장 Generic Timer

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

  • 각 core 마다 4개의 타이머가 인터럽트와 연결되어 있다.
  • 크게 두 종류의 Physical 타이머와 Virtual 타이머가 사용된다.
    • Physical 타이머는 뱅크되어 3개의 모드에서 각각 사용된다.
      • PL1 Secure 모드
      • PL1 Non-Secure 모드
      • PL2 하이퍼 모드
    • Virtual 타이머는 guest OS에 사용되며 하이퍼 바이저가 설정하는 voffset을 더해 사용된다.

 

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

  • 하이퍼 바이저용 Physical 카운터 관련 레지스터들(CNTHP_CTL, CNTHPCT, CNTHP_TVAL, CNTHP_CVAL) 생략

 

Generic 타이머 레지스터

64비트 레지스터

 

32비트 레지스터

 

Generic Timer 초기화  (per-cpu 클럭 이벤트 디바이스 등록)

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;
        }

        if (arch_timer_use_virtual) {
                ppi = arch_timer_ppi[VIRT_PPI];
                err = request_percpu_irq(ppi, arch_timer_handler_virt,
                                         "arch_timer", arch_timer_evt);
        } else {
                ppi = arch_timer_ppi[PHYS_SECURE_PPI];
                err = request_percpu_irq(ppi, arch_timer_handler_phys,
                                         "arch_timer", arch_timer_evt);
                if (!err && arch_timer_ppi[PHYS_NONSECURE_PPI]) {
                        ppi = arch_timer_ppi[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[PHYS_SECURE_PPI],
                                                arch_timer_evt);
                }
        }

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

        err = register_cpu_notifier(&arch_timer_cpu_nb);
        if (err)
                goto out_free_irq;

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

        /* Immediately configure the timer on the boot CPU */
        arch_timer_setup(this_cpu_ptr(arch_timer_evt));

        return 0;

out_unreg_notify:
        unregister_cpu_notifier(&arch_timer_cpu_nb);
out_free_irq:
        if (arch_timer_use_virtual)
                free_percpu_irq(arch_timer_ppi[VIRT_PPI], arch_timer_evt);
        else {
                free_percpu_irq(arch_timer_ppi[PHYS_SECURE_PPI],
                                arch_timer_evt);
                if (arch_timer_ppi[PHYS_NONSECURE_PPI])
                        free_percpu_irq(arch_timer_ppi[PHYS_NONSECURE_PPI],
                                        arch_timer_evt);
        }

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

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

  • 코드 라인 6~10에서 clock_event_device 구조체를 할당받아 온다. 메모리 할당이 실패하는 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 12~15에서 전역 arch_timer_use_virtual이 true인 경우 clock_event_device에 핸들러와 함께 등록한다.
  • 코드 라인 16~28에서 secure용 및 non-secure용 physical 타이머를 clock_event_device에 핸들러와 함께 등록한다.
  • 코드 라인 36~38에서 cpu 상태 변화에 따라 호출되도록 cpu notify chain에 arch_timer_cpu_notify() 함수를 등록한다.
    • secondary cpu가 on될 때마다 타이머가 초기화되고 off될 때마다 migration된다.
  • 코드 라인 40~42 CONFIG_CPU_PM 커널 옵션을 사용하는 경우 cpu 절전 상태 변화에 따라 호출되도록 cpu pm notify chain에 arch_timer_cpu_pm_notify() 함수를 등록한다.
  • 코드 라인 45에서 boot cpu에 대한 cp15용 generic 타이머 설정 및 per-cpu 인터럽트를 enable 한다.

 

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

 

arch_timer_setup()

drivers/clocksource/arm_arch_timer.c

static int arch_timer_setup(struct clock_event_device *clk)
{
        __arch_timer_setup(ARCH_CP15_TIMER, clk);

        if (arch_timer_use_virtual)
                enable_percpu_irq(arch_timer_ppi[VIRT_PPI], 0);
        else {
                enable_percpu_irq(arch_timer_ppi[PHYS_SECURE_PPI], 0);
                if (arch_timer_ppi[PHYS_NONSECURE_PPI])
                        enable_percpu_irq(arch_timer_ppi[PHYS_NONSECURE_PPI], 0);
        }

        arch_counter_set_user_access();
        if (IS_ENABLED(CONFIG_ARM_ARCH_TIMER_EVTSTREAM))
                arch_timer_configure_evtstream();

        return 0;
}

요청한 per-cpu 클럭 이벤트 디바이스에 대한 cp15용 generic 타이머 설정(초기 shutdown) 및 per-cpu 인터럽트를 enable 한다.

  • 코드 라인 3에서 per-cpu 클럭 이벤트 디바이스에 대한 cp15용 generic 타이머 설정을 한다. (초기 shutdown)
  • 코드 라인 5~11에서 사용하는 타이머의 per-cpu 인터럽트를 enable한다.
    • rpi2의 경우 인터럽트 컨트롤러의 bcm2836_arm_irqchip_unmask_timer_irq() 함수를 호출하여 enable 한다.
  • 코드 라인 13에서 타이머의 user access를 허용한다
    • CNTKCTL(Timer PL1 Control Register).PL0VCTEN 비트를 설정하여 Virtual 타이머의 PL0 access를 허용하게 한다.
  • 코드 라인 14~15에서 시스템에 따라 이벤트 스트림(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_CP15_TIMER) {
                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());
                if (arch_timer_use_virtual) {
                        clk->irq = arch_timer_ppi[VIRT_PPI];
                        clk->set_mode = arch_timer_set_mode_virt;
                        clk->set_next_event = arch_timer_set_next_event_virt;
                } else {
                        clk->irq = arch_timer_ppi[PHYS_SECURE_PPI];
                        clk->set_mode = arch_timer_set_mode_phys;
                        clk->set_next_event = arch_timer_set_next_event_phys;
                }
        } else {
                clk->features |= CLOCK_EVT_FEAT_DYNIRQ;
                clk->name = "arch_mem_timer";
                clk->rating = 400;
                clk->cpumask = cpu_all_mask;
                if (arch_timer_mem_use_virtual) {
                        clk->set_mode = arch_timer_set_mode_virt_mem;
                        clk->set_next_event =
                                arch_timer_set_next_event_virt_mem;
                } else {
                        clk->set_mode = arch_timer_set_mode_phys_mem;
                        clk->set_next_event =
                                arch_timer_set_next_event_phys_mem;
                }
        }

        clk->set_mode(CLOCK_EVT_MODE_SHUTDOWN, clk);

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

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

  • 코드 라인 4에서 generic 타이머에 oneshot 기능을 부여한다.
  • 코드 라인 6~8에서 보조프로세서 cp15로 제어되는 타이머인 경우이면서 c3stop 기능이 있는 경우 features에 C3STOP 플래그를 추가한다.
    • rpi2: c3stop을 사용하지 않는다.
  • 코드 라인 9~11에서 “arch_sys_timer”라는 이름으로 현재 cpu에 대해서만 cpumask를 설정하고 rating을 450으로 설정한다.
  • 코드 라인 12~20에서 virtual 타이머를 사용하는 경우 해당 인터럽트 및 핸들러를 설정한다. 그렇지 않은 경우secure physical 타이머에 대한 인터럽트 및 핸들러를 설정한다.
  • 코드 라인 21~25에서 메모리 mapped 타이머를 지원하는 경우 feauters에 DYNIRQ 플래그를 추가하고 “arch_mem_timer”라는 이름으로 전체 cpu에 대해서 cpumask를 설정하고 rating을 400으로 설정한다.
  • 코드 라인 26~35에서 virtual 타이머를 사용하는 경우 해당 인터럽트 및 핸들러를 설정한다. 그렇지 않은 경우secure physical 타이머에 대한 인터럽트 및 핸들러를 설정한다.
  • 코드 라인 37에서 현재 cpu의 타이머 출력을 정지시킨다.
  • 코드 라인 39에서 클럭 이벤트 디바이스 설정 및 등록을 진행한다.

 

Clock Events 설정 및 등록 -1-

clockevents_config_and_register()

kernel/time/clockevents.c

/**
 * clockevents_config_and_register - Configure and register a clock event device
 * @dev:        device to register
 * @freq:       The clock frequency
 * @min_delta:  The minimum clock ticks to program in oneshot mode
 * @max_delta:  The maximum clock ticks to program in oneshot mode
 *
 * min/max_delta can be 0 for devices which do not support oneshot mode.
 */
void clockevents_config_and_register(struct clock_event_device *dev,
                                     u32 freq, unsigned long min_delta,
                                     unsigned long max_delta)
{
        dev->min_delta_ticks = min_delta; 
        dev->max_delta_ticks = max_delta;
        clockevents_config(dev, freq);
        clockevents_register_device(dev);
}
EXPORT_SYMBOL_GPL(clockevents_config_and_register);

요청 주파수의 clock_event_device를 최소 클럭 틱에서 최대 클럭 틱 값 사이에서 동작하도록 등록한다.

  • rpi2: max_delta_ticks=0x7fff_ffff, 19.2Mhz, 1000HZ 사용 시 최대 ns=약 111.8초
  • tick 값들은 32bit long 형을 사용하고 위의 rpi2와 같이 한계치인 0x7fff_ffff 값을 사용할 때 틱의 프로그램은 최대 111.8초를 초과할 수 없다.

 

clockevents_config()

kernel/time/clockevents.c

void clockevents_config(struct clock_event_device *dev, u32 freq)
{
        u64 sec;

        if (!(dev->features & CLOCK_EVT_FEAT_ONESHOT))
                return;

        /*
         * Calculate the maximum number of seconds we can sleep. Limit
         * to 10 minutes for hardware which can program more than
         * 32bit ticks so we still get reasonable conversion values.
         */
        sec = dev->max_delta_ticks;
        do_div(sec, freq);
        if (!sec)
                sec = 1;
        else if (sec > 600 && dev->max_delta_ticks > UINT_MAX)
                sec = 600;
        
        clockevents_calc_mult_shift(dev, freq, sec);
        dev->min_delta_ns = cev_delta2ns(dev->min_delta_ticks, dev, false);
        dev->max_delta_ns = cev_delta2ns(dev->max_delta_ticks, dev, true);
}

요청 주파수의 clock_event_device에 대해 mult/shift 값 및 min_delta_ns/max_delta_ns 값을 조정한다.

  • 코드 라인 5~6에서 디바이스가 oneshot 모드를 지원하지 않는 경우 함수를 빠져나간다.
  • 코드 라인 13~18에서 최대 max_delta_ticks를 freq로 나누면 소요 초가 산출되는데 32bit tick을 초과한 경우 최대 600초로 제한한다.
    • 10분 이상의 오차 보정을 할 필요 없어서 제한한다.
  • 코드 라인 20에서 max_delta_ticks, 주파수와 소요시간(sec)으로 mult 및 shift 값을 산출한다.
  • 코드 라인 21~22에서 min_delta_ticks, max_delta_ticks 값과 산출된 mult/shift 값으로 min_delta_ns와 max_delta_ns 값을 산출한다.
    • min_delta_ns 값은 최하 1000 ns(1 ms) 이상으로 한다.

 

다음 그림은 192.Mhz로 클럭 이벤트 디바이스의 mult/shift, min(max)_delta_ns 값을 설정하는 것을 보여준다.

 

clockevents_calc_mult_shift()

include/linux/clockchips.h

static inline void
clockevents_calc_mult_shift(struct clock_event_device *ce, u32 freq, u32 minsec)
{
        return clocks_calc_mult_shift(&ce->mult, &ce->shift, NSEC_PER_SEC,
                                      freq, minsec);
}

1초 -> freq로 변환 시 요청 기간(sec) 동안 필요한 mult 및 shift 값을 산출한다.

  • 예) 1G -> 19.2Mhz에 111초인 경우
    • mult=0x682_aaab, shift=32

 

cev_delta2ns()

kernel/time/clockevents.c

static u64 cev_delta2ns(unsigned long latch, struct clock_event_device *evt,
                        bool ismax)
{
        u64 clc = (u64) latch << evt->shift;
        u64 rnd;

        if (unlikely(!evt->mult)) {
                evt->mult = 1;
                WARN_ON(1);
        }
        rnd = (u64) evt->mult - 1;

        /*
         * Upper bound sanity check. If the backwards conversion is
         * not equal latch, we know that the above shift overflowed.
         */
        if ((clc >> evt->shift) != (u64)latch)
                clc = ~0ULL;

        /*
         * Scaled math oddities:
         *
         * For mult <= (1 << shift) we can safely add mult - 1 to
         * prevent integer rounding loss. So the backwards conversion
         * from nsec to device ticks will be correct.
         *
         * For mult > (1 << shift), i.e. device frequency is > 1GHz we
         * need to be careful. Adding mult - 1 will result in a value
         * which when converted back to device ticks can be larger
         * than latch by up to (mult - 1) >> shift. For the min_delta
         * calculation we still want to apply this in order to stay
         * above the minimum device ticks limit. For the upper limit
         * we would end up with a latch value larger than the upper
         * limit of the device, so we omit the add to stay below the
         * device upper boundary.
         *
         * Also omit the add if it would overflow the u64 boundary.
         */
        if ((~0ULL - clc > rnd) &&
            (!ismax || evt->mult <= (1ULL << evt->shift)))
                clc += rnd;

        do_div(clc, evt->mult);

        /* Deltas less than 1usec are pointless noise */
        return clc > 1000 ? clc : 1000;
}

latch 값을 nano 초 값으로 변환한다. 정확도를 위해 주파수에 따라 다음의 수식을 사용한다.

  • 주파수가 1Ghz 이하인 경우 mult로 나눌 때 반올림 처리한다. 이렇게 해야 nsec에서 장치 틱으로 역방향 시 변환이 더 정확해진다.
    • (latch << shift + mult – 1) / mult = ns
  • 주파수가 1Ghz를 초과하는 경우 그대로 mult로 나눈다.
    • (latch << shift) / mult = ns

 

Clock Events 설정 및 등록 -2-

clockevents_register_device()

kernel/time/clockevents.c

/**
 * clockevents_register_device - register a clock event device
 * @dev:        device to register
 */
void clockevents_register_device(struct clock_event_device *dev)
{
        unsigned long flags;

        BUG_ON(dev->mode != CLOCK_EVT_MODE_UNUSED);
        if (!dev->cpumask) {
                WARN_ON(num_possible_cpus() > 1);
                dev->cpumask = cpumask_of(smp_processor_id());
        }

        raw_spin_lock_irqsave(&clockevents_lock, flags);

        list_add(&dev->list, &clockevent_devices);
        tick_check_new_device(dev);
        clockevents_notify_released();

        raw_spin_unlock_irqrestore(&clockevents_lock, flags);
}
EXPORT_SYMBOL_GPL(clockevents_register_device);

클럭 이벤트 디바이스를 등록한다.

  • 코드 라인 10~13에서 클럭 이벤트 디바이스가 동작할 cpu를 현재 태스크가 수행 중인 cpu 번호로 설정한다. (cpumask 형태로 지정)
  • 코드 라인 17에서 clockevent_devices 리스트에 클럭 이벤트 디바이스를 등록한다.
  • 코드 라인 18에서 기존 tick 디바이스보다 새 tick 디바이스가 더 좋은 rating 등급인 경우 변경하여 사용할지 체크한다.
    • 처음 호출 시에는 요청 디바이스가 tick 디바이스로 사용된다.
    • nohz 구현을 위해 경우에 따라 등록되는 클럭 이벤트 디바이스가 틱 브로드캐스트 디바이스로 동작할 수도 있다.
  • 코드 라인 19에서 clockevents_released 리스트에 등록된 클럭 이벤트 디바이스들을 제거하고 clockevent_devices 리스트에 추가한 후 새로운 tick 디바이스로 사용할 수 있는지 체크한다.

 

clockevents_notify_released()

kernel/time/clockevents.c

/*
 * Called after a notify add to make devices available which were
 * released from the notifier call.
 */
static void clockevents_notify_released(void)
{
        struct clock_event_device *dev;

        while (!list_empty(&clockevents_released)) {
                dev = list_entry(clockevents_released.next,
                                 struct clock_event_device, list);
                list_del(&dev->list);
                list_add(&dev->list, &clockevent_devices);
                tick_check_new_device(dev);
        }
}

clockevents_released 리스트에 등록된 클럭 이벤트 디바이스들을 제거하고 clockevent_devices 리스트에 추가한 후 새로운 tick 디바이스로 사용할 수 있는지 체크한다.

 

Clock Events 변경(Exchange)

clockevents_exchange_device()

kernel/time/clockevents.c

/**
 * clockevents_exchange_device - release and request clock devices
 * @old:        device to release (can be NULL) 
 * @new:        device to request (can be NULL)
 *
 * Called from the notifier chain. clockevents_lock is held already
 */     
void clockevents_exchange_device(struct clock_event_device *old,
                                 struct clock_event_device *new)
{
        unsigned long flags;     

        local_irq_save(flags);       
        /*      
         * Caller releases a clock event device. We queue it into the
         * released list and do a notify add later.
         */
        if (old) {
                module_put(old->owner);
                clockevents_set_mode(old, CLOCK_EVT_MODE_UNUSED);
                list_del(&old->list); 
                list_add(&old->list, &clockevents_released);
        }

        if (new) {
                BUG_ON(new->mode != CLOCK_EVT_MODE_UNUSED);
                clockevents_shutdown(new);
        }
        local_irq_restore(flags);
}

old 클럭 이벤트 디바이스를 release 하고 new 클럭 이벤트 디바이스로 교체한다. 만일 교체한 new 클럭 이벤트 디바이스가 동작 중인 경우 shutdown 상태로 변경한다.

  • 코드 라인 18~23에서 old 클럭 이벤트 디바이스를 unused 모드로 설정하고 clockevent_devices 리스트에서 제거한 후 clockevents_release 리스트에 추가한다.
  • 코드 라인 25~28에서 new 클럭 이벤트 디바이스를 shutdown 설정한다.

 

clockevents_shutdown()

kernel/time/clockevents.c

/**
 * clockevents_shutdown - shutdown the device and clear next_event
 * @dev:        device to shutdown
 */
void clockevents_shutdown(struct clock_event_device *dev)
{
        clockevents_set_mode(dev, CLOCK_EVT_MODE_SHUTDOWN);
        dev->next_event.tv64 = KTIME_MAX;
}

요청한 클럭 이벤트 디바이스를 shutdown 설정한다.

 

clockevents_set_mode()

kernel/time/clockevents.c

/**
 * clockevents_set_mode - set the operating mode of a clock event device
 * @dev:        device to modify
 * @mode:       new mode
 *
 * Must be called with interrupts disabled !
 */
void clockevents_set_mode(struct clock_event_device *dev,
                                 enum clock_event_mode mode)
{
        if (dev->mode != mode) {
                dev->set_mode(mode, dev);
                dev->mode = mode;
        
                /*
                 * A nsec2cyc multiplicator of 0 is invalid and we'd crash
                 * on it, so fix it up and emit a warning:
                 */
                if (mode == CLOCK_EVT_MODE_ONESHOT) {
                        if (unlikely(!dev->mult)) {
                                dev->mult = 1;
                                WARN_ON(1);
                        }
                }
        }       
}

요청한 클럭 이벤트 디바이스의 모드를 설정한다.

  • 코드 라인 11~13에서 현재 클럭 이벤트 디바이스의 모드와 다른 모드 설정이 요청되면 모드를 변경하고 디바이스에 등록된 모드 변경 함수도 호출한다.
    • rpi2 예) arch_timer_set_mode_virt()
  • 코드 라인 19~24에서 oneshot 모드 설정을 요청한 하였고 mult 값이 0인 경우 1로 변경한다.

 

arch_timer_set_mode_virt()

drivers/clocksource/arm_arch_timer.c

static void arch_timer_set_mode_virt(enum clock_event_mode mode,
                                     struct clock_event_device *clk)
{
        timer_set_mode(ARCH_TIMER_VIRT_ACCESS, mode, clk);
}

CNTV_CTL(Virtual Counter Control Register)의 모드를 설정한다.

  • mode에 unused와 shutdown만 지원한다.

 

timer_set_mode()

drivers/clocksource/arm_arch_timer.c

static __always_inline void timer_set_mode(const int access, int mode,
                                  struct clock_event_device *clk)
{
        unsigned long ctrl;
        switch (mode) {
        case CLOCK_EVT_MODE_UNUSED:
        case CLOCK_EVT_MODE_SHUTDOWN:
                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);
                break;
        default:
                break;
        }
}

CNTV_CTL(Virtual Counter Control Register)의 모드를 설정한다. 요청한 모드가 unused와 shutdown인 경우에 한하여 CNTV_CTL(Virtual Counter Control Register)의 enable 비트를 클리어하여 타이머를 멈추게한다.

 

Clock Events 프로그램 이벤트

clockevents_program_event()

kernel/time/clockevents.c

/**
 * clockevents_program_event - Reprogram the clock event device.
 * @dev:        device to program
 * @expires:    absolute expiry time (monotonic clock)
 * @force:      program minimum delay if expires can not be set
 *
 * Returns 0 on success, -ETIME when the event is in the past.
 */                              
int clockevents_program_event(struct clock_event_device *dev, ktime_t expires,
                              bool force)
{
        unsigned long long clc;
        int64_t delta;
        int rc;

        if (unlikely(expires.tv64 < 0)) {
                WARN_ON_ONCE(1);
                return -ETIME;
        }

        dev->next_event = expires;

        if (dev->mode == CLOCK_EVT_MODE_SHUTDOWN)
                return 0;

        /* Shortcut for clockevent devices that can deal with ktime. */
        if (dev->features & CLOCK_EVT_FEAT_KTIME)
                return dev->set_next_ktime(expires, dev);

        delta = ktime_to_ns(ktime_sub(expires, ktime_get()));
        if (delta <= 0)
                return force ? clockevents_program_min_delta(dev) : -ETIME;

        delta = min(delta, (int64_t) dev->max_delta_ns);
        delta = max(delta, (int64_t) dev->min_delta_ns);

        clc = ((unsigned long long) delta * dev->mult) >> dev->shift;
        rc = dev->set_next_event((unsigned long) clc, dev);

        return (rc && force) ? clockevents_program_min_delta(dev) : rc;
}

클럭 이벤트 디바이스의 만료시간을 재프로그램 한다. force=1인 경우 요청한 만료 시간이 이미 지난 경우라 하더라도 최소 시간(min_delay_ns)이내에 이벤트가 발생하도록 프로그램한다.

  • 코드 라인 16~19에서 낮은 확률로 만료시간이 0보다 작은 경우 에러 값으로 -ETIME을 반환한다.
  • 코드 라인 21에서 디바이스의 next_event에 만료 시간을 대입한다.
  • 코드 라인 22~24에서 디바이스의 모드가 shutdown 상태인 경우 더 이상 진행할 필요 없으므로 성공(0)을 반환한다.
  • 코드 라인 27~28에서 ktime 기능이 있는 경우 다음 이벤트의 만료시간을 설정한다.
  • 코드 라인 30~32에서 요청한 만료 시간이 이미 현재 시간을 지난 경우 force 값에 따라 min_delta_ns 값 또는 -ETIME 값을 반환한다.
  • 코드 라인 34~35에서 delta 값이 min_delta_ns ~ max_delta_ns 범위를 벗어나는 경우 조정(clamp)한다.
  • 코드 라인 37~38에서 (delta * mult) >> shift를 산출한 값으로 다음 이벤트의 만료시간을 설정한다.
  • 코드 라인 40에서 에러이면서  force=1인 경우 min_delta 값으로 다음 이벤트의 만료시간을 설정한다.

 

다음 그림은 최소 지연 시간 1000us으로 이벤트를 프로그램하는 것을 보여준다.

 

clockevents_program_min_delta()

kernel/time/clockevents.c

/**
 * clockevents_program_min_delta - Set clock event device to the minimum delay.
 * @dev:        device to program
 *
 * Returns 0 on success, -ETIME when the retry loop failed.
 */
static int clockevents_program_min_delta(struct clock_event_device *dev)
{
        unsigned long long clc;
        int64_t delta;
        int i;

        for (i = 0;;) {
                delta = dev->min_delta_ns;
                dev->next_event = ktime_add_ns(ktime_get(), delta);

                if (dev->mode == CLOCK_EVT_MODE_SHUTDOWN)
                        return 0;

                dev->retries++;
                clc = ((unsigned long long) delta * dev->mult) >> dev->shift;
                if (dev->set_next_event((unsigned long) clc, dev) == 0)
                        return 0;

                if (++i > 2) {
                        /*
                         * We tried 3 times to program the device with the
                         * given min_delta_ns. Try to increase the minimum
                         * delta, if that fails as well get out of here.
                         */
                        if (clockevents_increase_min_delta(dev))
                                return -ETIME;
                        i = 0;
                }
        }
}

클럭 이벤트 디바이스를 최소 딜레이 시간(min_delay_ns)으로 프로그램한다.

  • 코드 라인 13~15에서 현재 시간 보다 min_delta_ns 값을 더해 다음 이벤트의 만료 시간을 설정한다.
  • 코드 라인 17~18에서 디바이스의 모드가 shutdown 상태인 경우 아직 동작시킬 수 없으므로 성공(0)으로 함수를 빠져나간다.
  • 코드 라인 20~23에서 retries 값을 증가시키고 (delta * mult) >> shift 값으로 다음 이벤트의 만료 시간을 설정하고 함수를 성공(0)으로 빠져나간다.
  • 코드 라인 25~34에서 주어진 min_delta_ns 값으로 3번을 다시 시도해보고 안되는 경우 min_delta_ns 값을 증가시키고 다시 시도한다.

 

clockevents_increase_min_delta()

kernel/time/clockevents.c

/**
 * clockevents_increase_min_delta - raise minimum delta of a clock event device
 * @dev:       device to increase the minimum delta
 *
 * Returns 0 on success, -ETIME when the minimum delta reached the limit.
 */
static int clockevents_increase_min_delta(struct clock_event_device *dev)
{
        /* Nothing to do if we already reached the limit */
        if (dev->min_delta_ns >= MIN_DELTA_LIMIT) {
                printk_deferred(KERN_WARNING
                                "CE: Reprogramming failure. Giving up\n");
                dev->next_event.tv64 = KTIME_MAX;
                return -ETIME;
        }

        if (dev->min_delta_ns < 5000)
                dev->min_delta_ns = 5000;
        else
                dev->min_delta_ns += dev->min_delta_ns >> 1;

        if (dev->min_delta_ns > MIN_DELTA_LIMIT)
                dev->min_delta_ns = MIN_DELTA_LIMIT;

        printk_deferred(KERN_WARNING
                        "CE: %s increased min_delta_ns to %llu nsec\n",
                        dev->name ? dev->name : "?",
                        (unsigned long long) dev->min_delta_ns);
        return 0;
}

클럭 이벤트 디바이스의 최소 딜레이 시간(min_delta_ns)을 증가시킨다. 호출 될 때마다 5us부터 시작하여 1.5배씩 증가하며 마지막에 최대 1 jiffies 만큼 상승한다. 그 이후에는 -ETIME 에러를 반환한다.

  • 코드 라인 10~15에서 min_delta_ns 값이 한계값(1 jiffies) 이상이되면 경고 메시지 출력과 함께 에러 -ETIME을 반환한다.
    • MIN_DELTA_LIMIT
      • (NSEC_PER_SEC / HZ) 값으로 1 jiffies 소요 시간과 동일하다.
  • 코드 라인 17~20에서 min_delta_ns 값이 5us 미만인 경우 5us로 하고 그렇지 않은 경우 현재 min_delta_ns 값에서 50%를 증가시킨다.
  • 코드 라인 22~23에서 min_delta_ns 값이 한계값을 초과하면 한계값으로 설정한다.
  • 코드 라인 25~29에서 min_delta_ns 값이 증가되었음을 경고 메시지로 출력하고 성공(0)으로 반환한다.

 

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;
}

CNTV_TVAL(Virtual Counter TImer Value) 값을 설정한다.

 

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);
}

CNTV_TVAL(Virtual Counter TImer Value) 값을 설정한 후 타이머를 가동시킨다.

  • CNTV_TVAL(Virtual Counter Timer Value)에 evt 값을 설정한다.
  • CNTV_CTL(Virtual Counter Control Register)의 enable 비트를 설정하고 masked 비트를 클리어하여 타이머를 가동시킨다.

 

이벤트 스트림 설정 및 가동

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한다.

 

다음 그림은 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);
        elf_hwcap |= HWCAP_EVTSTRM;
#ifdef CONFIG_COMPAT
        compat_elf_hwcap |= COMPAT_HWCAP_EVTSTRM;
#endif
}

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

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

 

arch_timer_get_cntkctl()

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) 레지스터 값을 읽어온다.

 

구조체

clock_events_device 구조체

include/linux/clockchips.h

/**
 * struct clock_event_device - clock event device descriptor
 * @event_handler:      Assigned by the framework to be called by the low
 *                      level handler of the event source
 * @set_next_event:     set next event function using a clocksource delta
 * @set_next_ktime:     set next event function using a direct ktime value
 * @next_event:         local storage for the next event in oneshot mode
 * @max_delta_ns:       maximum delta value in ns
 * @min_delta_ns:       minimum delta value in ns
 * @mult:               nanosecond to cycles multiplier
 * @shift:              nanoseconds to cycles divisor (power of two)
 * @mode:               operating mode assigned by the management code
 * @features:           features
 * @retries:            number of forced programming retries
 * @set_mode:           set mode function
 * @broadcast:          function to broadcast events
 * @min_delta_ticks:    minimum delta value in ticks stored for reconfiguration
 * @max_delta_ticks:    maximum delta value in ticks stored for reconfiguration
 * @name:               ptr to clock event name
 * @rating:             variable to rate clock event devices
 * @irq:                IRQ number (only for non CPU local devices)
 * @bound_on:           Bound on CPU
 * @cpumask:            cpumask to indicate for which CPUs this device works
 * @list:               list head for the management code
 * @owner:              module reference
 */
struct clock_event_device {
        void                    (*event_handler)(struct clock_event_device *);
        int                     (*set_next_event)(unsigned long evt,
                                                  struct clock_event_device *);
        int                     (*set_next_ktime)(ktime_t expires,
                                                  struct clock_event_device *);
        ktime_t                 next_event;
        u64                     max_delta_ns;
        u64                     min_delta_ns;
        u32                     mult;
        u32                     shift;
        enum clock_event_mode   mode;
        unsigned int            features;
        unsigned long           retries;

        void                    (*broadcast)(const struct cpumask *mask);
        void                    (*set_mode)(enum clock_event_mode mode,
                                            struct clock_event_device *);
        void                    (*suspend)(struct clock_event_device *);
        void                    (*resume)(struct clock_event_device *);
        unsigned long           min_delta_ticks;
        unsigned long           max_delta_ticks;

        const char              *name;
        int                     rating;
        int                     irq;
        int                     bound_on;
        const struct cpumask    *cpumask;
        struct list_head        list;
        struct module           *owner;
} ____cacheline_aligned;
  • (*event_handler)
    • 만료 시간 시 호출될 이벤트 핸들러 함수가 등록된다.
  • (*set_next_event)
    • 다음 이벤트의 만료 시간을 설정할 클럭 이벤트 드라이버 함수가 등록된다.
  • (*set_next_ktime)
    • 다음 이벤트의 만료 시간을 ktime 으로 지정하여 설정하는 클럭 이벤트 드라이버 함수가 등록된다.
  • next_event
    • oneshot 모드에서 다음 이벤트가 만료될 ktime 값
  • max_delta_ns
    • 최대 프로그래밍 허용한 다음 이벤트의 최대 지연 nano 초
  • min_delta_ns
    • 최소 프로그래밍 허용한 다음 이벤트의 최소 지연 nano 초
  • mult
    • 1 ns를 만들기 위해 cycle 카운터에 곱할 값이며 shift와 같이 사용된다.
    • 1 ns = (1 cycle 카운터 x mult) >> shift
    • 예) 2Ghz, 2개의 cycle = 1 ns일 경우
      • mult=2, shift=0
  • shift
    • 1 ns를 만들기 위해 cycle 카운터에 1 << shift 값으로 분배할 값이며 mult와 같이 사용된다.
    • 예) 19.2Mhz, 1개의 cycle = 52 ns일 경우
      • mult=0x4ea_4a8c, shift=32
  • mode
    • 클럭 이벤트 운영 모드
      • CLOCK_EVT_MODE_UNUSED(0)
      • CLOCK_EVT_MODE_SHUTDOWN(1)
      • CLOCK_EVT_MODE_PERIODIC(2)
      • CLOCK_EVT_MODE_ONESHOT(3)
      • CLOCK_EVT_MODE_RESUME(4)
  • features
    • 기능 플래그들로 이 글의 처음에 자세히 설명하였다
  • retries
    • 프로그래밍 재시도 수
  • (*broadcast)
    • 이벤트를 브로드캐스트하는 함수
  • (*set_mode)
    • 모드를 설정하는 함수
  • (*suspend)
    • 타이머 디바이스의 suspend 기능을 지원하는 함수
  • (*resume)
    • suspend된 타이머 디바이스에 대해 resume을 지원하는 함수
  • min_delta_ticks
    • 최소 프로그래밍 허용한 다음 tick 수
  • max_delta_ticks
    • 최대 프로그래밍 허용한 다음 tick 수
  • *name
    • 클럭 이벤트 명
  • rating
    • 정밀도 등급
  • irq
    • irq 번호 (per-cpu 디바이스가 아닌 경우)
  • bound_on
  • cpumask
    • 이 클럭 이벤트 디바이스가 동작할  cpumask
  • list
    • 클럭 이벤트를 관리하고 있는 리스트에 연결되어 사용된다.
  • owner
    • 모듈 참조자

 

 참고

답글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.