ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] PM - Wakeup count
    Linux/kernel 2023. 8. 29. 20:00

    글의 참고

    - http://www.wowotech.net/pm_subsystem/wakeup_count.html

    - https://www.cnblogs.com/hellokitty2/category/1339324.html

    - https://lishiwen4.github.io/categories/

    - https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-power

    - https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-power

    - https://lwn.net/Articles/393314/

    - https://lwn.net/Articles/416690/


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.


    글의 내용

    - wakeup count

    : `wakeup count`의 등장 배경은 시스템이 suspend 모드로 진입하는 시점에 wakeup event가 발생할 경우, 해당 이벤트가 사라지는 것에 대한 해결책으로 등장했다. 즉, suspend와 wakeup event 사이에 동기화 문제를 해결하기 위해 등장했다.

     Rafael is trying to solve the problem of "wakeup events" (events requiring action which would wake a suspended device) being lost if they show up while the system is suspending.
    ...

    As with some other scenarios which have been posted, Rafael is assuming the existence of a user-space power management daemon which would decide when to suspend the system. This decision would be made when the daemon knows that no important user-space program has work to do. Without extra help, though, there will be a window between the time that the daemon decides to suspend the system and when that suspend actually happens; a wakeup event which arrives within that window could be lost, or at least improperly delayed until after the system is resumed again. But, with the wakeup_count mechanism, the kernel would notice when this kind of race had happened and abort the suspend process, allowing user space to process the new event.
    ...

    - 참고 : https://lwn.net/Articles/393314/

     

    - wakeup count  기능

    : wakeup count 목적은 wakeup event가 발생했을 때, 진행중인 suspend가 있다면 abort 시키겠다는 의도다. 이 의미는 suspend와 wakeup을 동기화 시키겠다는 의미로 받아들이면 된다. wakeup count의 동작 시점은 `/sys/power/state`에 쓰기 전과 쓴 후로 나누어 볼 수 있다.

    * This difficulty may be overcome if user space uses 'wakeup_count' before
     * writing to 'state'.  It first should read from 'wakeup_count' and store
     * the read value.  Then, after carrying out its own preparations for the system
     * transition to a sleep state, it should write the stored value to
     * 'wakeup_count'.  If that fails, at least one wakeup event has occurred since
     * 'wakeup_count' was read and 'state' should not be written to.  Otherwise, it
     * is allowed to write to 'state', but the transition will be aborted if there
     * are any wakeup events detected after 'wakeup_count' was written to.
     */
    
    - 참고 : https://elixir.bootlin.com/linux/latest/source/kernel/power/main.c

    : 먼저 `echo mem > /sys/power/state`을 하기전에 과정을 `/sys/power/wakeup_count`를 읽어서, `wakeup events framework`에 전달한다. count를 전달받은 wakeup events framework는 해당 카운트를 `saved wakeup counts`에 저장한다. 그리고, 시스템이 suspend 상태로 진입할 수 있도록 준비 과정을 진행한다(이 부분이 위에서 Rafael가 얘기한 `a window between time ... ` 이다). 중요한 건 아직 시스템은 suspend 상태가 아니라는 것이다. 이 상태에서 `saved wakeup counts` 값을 `/sys/power/wakeup_count`에 쓴다. 만약, 실패하면 이건 suspend 준비 과정에서 wakeup events가 발생했다는 의미로 suspend는 abort 된다. 이 시점에서는 `mem`값이 `state`에 아예 써지지도 않는 상태다. 반대로, 성공을 하면, `mem`이 `state`에 써진다. 이 때 부터는 실제 `suspending` 상태가 된다. 그러나, 이 때도 검사는 진행된다. 이제 부터가 `/sys/power/state` 쓴 후 과정이다.

    1" suspend 과정에서도 wakeup events framework는 정상적으로 동작을 한다. 그래서 wakeup events가 발생하면, 해당 이벤트를 wakeup events counts에 추가한다.

    2" suspend 과정중에 몇몇 지점에서는 wakeup events framework에서 제공하는 `pm_wakeup_pending` 함수를 호출하는 루틴이 있다. 이 함수는 suspend 진입 전에 기록된 saved wakeup counts와 현재 wakeup counts를 비교해서 다르면 suspend 과정에서 wakeup events가 발생했다고 인지하여 suspend를 종료한다.

     

    - wakeup count 동작

    : wakeup_count는 system 측면과 device 측면으로 나누어 볼 수 있다. 예를 들어, 시스템 측면의 wakeup_count는 `/sys/power/wakeup_count` 파일과 연관되어 있다. `/sys/power/wakeup_count`는 시스템 전반에서 발생한 wakeup_event 의 개수를 카운트한 값을 의미한다.

    The /sys/power/wakeup_count file allows user space to put the system into a sleep state while taking into account the concurrent arrival of wakeup events. Reading from it returns the current number of registered wakeup events and it blocks if some wakeup events are being processed at the time the file is read from. Writing to it will only succeed if the current number of wakeup events is equal to the written value and, if successful, will make the kernel abort a subsequent transition to a sleep state if any wakeup events are reported after the write has returned.

    - 참고 : https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-power

     

    : `/sys/power/wakeup_count`는 `/kernel/power/main.c`  함수에 정의되어 있다. 사실 이미 앞에서 설명을 했지만, 주석에 `wakeup_count`에 대한 자세한 내용이 잘 설명되어 있다. 아래의 `wakeup_count_show`, `wakeup_count_store` 함수가 실제로 sysfs에서 `cat /sys/power/wakeup_count` 속성에 매핑되는 함수들이다.

    //kernel/power/main.c - v6.5
    #ifdef CONFIG_PM_SLEEP
    /*
     * The 'wakeup_count' attribute, along with the functions defined in
     * drivers/base/power/wakeup.c, provides a means by which wakeup events can be
     * handled in a non-racy way.
     *
     * If a wakeup event occurs when the system is in a sleep state, it simply is
     * woken up.  In turn, if an event that would wake the system up from a sleep
     * state occurs when it is undergoing a transition to that sleep state, the
     * transition should be aborted.  Moreover, if such an event occurs when the
     * system is in the working state, an attempt to start a transition to the
     * given sleep state should fail during certain period after the detection of
     * the event.  Using the 'state' attribute alone is not sufficient to satisfy
     * these requirements, because a wakeup event may occur exactly when 'state'
     * is being written to and may be delivered to user space right before it is
     * frozen, so the event will remain only partially processed until the system is
     * woken up by another event.  In particular, it won't cause the transition to
     * a sleep state to be aborted.
     *
     * This difficulty may be overcome if user space uses 'wakeup_count' before
     * writing to 'state'.  It first should read from 'wakeup_count' and store
     * the read value.  Then, after carrying out its own preparations for the system
     * transition to a sleep state, it should write the stored value to
     * 'wakeup_count'.  If that fails, at least one wakeup event has occurred since
     * 'wakeup_count' was read and 'state' should not be written to.  Otherwise, it
     * is allowed to write to 'state', but the transition will be aborted if there
     * are any wakeup events detected after 'wakeup_count' was written to.
     */
    
    static ssize_t wakeup_count_show(struct kobject *kobj,
    				struct kobj_attribute *attr,
    				char *buf)
    {
    	unsigned int val;
    
    	return pm_get_wakeup_count(&val, true) ?
    		sprintf(buf, "%u\n", val) : -EINTR;
    }
    
    static ssize_t wakeup_count_store(struct kobject *kobj,
    				struct kobj_attribute *attr,
    				const char *buf, size_t n)
    {
    	unsigned int val;
    	int error;
    
    	error = pm_autosleep_lock();
    	if (error)
    		return error;
    
    	if (pm_autosleep_state() > PM_SUSPEND_ON) {
    		error = -EBUSY;
    		goto out;
    	}
    
    	error = -EINVAL;
    	if (sscanf(buf, "%u", &val) == 1) {
    		if (pm_save_wakeup_count(val))
    			error = n;
    		else
    			pm_print_active_wakeup_sources();
    	}
    
     out:
    	pm_autosleep_unlock();
    	return error;
    }
    
    power_attr(wakeup_count);

    : 위의 함수들에서 각각 `pm_get_wakeup_count`와 `pm_save_wakeup_count` 함수들이 보일 것이다. 이 함수들이 실제 wakeup count 관련한 핵심함수들이다.

     

    : `pm_get_wakeup_count`는 현재 처리 중인 웨이크-업 이벤트(wakeup event in progress)가 있는지를 알려준다. `split_counters` 함수를 통해, 현재까지 처리된 웨이크-업와 현재 처리중인 웨이크-업 이벤트를 얻어낸다. 그리고, `!inpr`을 통해서 현재 처리중인 웨이크-업 이벤트 여부를 반환한다. `pm_get_wakeup_count`의 두 번째 인자가 true면, 현재 처리 중인 웨이크-업 이벤트가 0이 될 때 까지, 대기한다.  

    //drivers/base/power/wakeup.c v6.5
    /**
     * pm_get_wakeup_count - Read the number of registered wakeup events.
     * @count: Address to store the value at.
     * @block: Whether or not to block.
     *
     * Store the number of registered wakeup events at the address in @count.  If
     * @block is set, block until the current number of wakeup events being
     * processed is zero.
     *
     * Return 'false' if the current number of wakeup events being processed is
     * nonzero.  Otherwise return 'true'.
     */
    bool pm_get_wakeup_count(unsigned int *count, bool block)
    {
    	unsigned int cnt, inpr;
    
    	if (block) {
    		DEFINE_WAIT(wait);
    
    		for (;;) {
    			prepare_to_wait(&wakeup_count_wait_queue, &wait,
    					TASK_INTERRUPTIBLE);
    			split_counters(&cnt, &inpr);
    			if (inpr == 0 || signal_pending(current))
    				break;
    			pm_print_active_wakeup_sources();
    			schedule();
    		}
    		finish_wait(&wakeup_count_wait_queue, &wait);
    	}
    
    	split_counters(&cnt, &inpr);
    	*count = cnt;
    	return !inpr;
    }

    : `pm_get_wakeup_count` 함수가 false를 반환하면, 시스템이 suspend로 진입하기에는 적합하지 않다는 뜻이다.

     

    : `pm_save_wakeup_count` 함수는 `wakeup_count_store` 함수에서 호출되는 함수로 전달된 count 값과 현재 registered wakeup events count 같고 wakeup events in progress가 없으면, 전달받은 count를 saved_count에 저장하고  `events_check_enabled`를 true로 설정하는 함수다. 여기서 만약, count가 cnt와 다르거나(wakeup count를 읽고 쓰는 과정에서 wakeup event 발생) 혹은 `wakeup event in progress`가 있는 경우, 현재 상태는 suspend로 진입하기 어렵다고 판단하고 false를 반환한다.

    // drivers/base/power/wakeup.c - v6.5
    /**
     * pm_save_wakeup_count - Save the current number of registered wakeup events.
     * @count: Value to compare with the current number of registered wakeup events.
     *
     * If @count is equal to the current number of registered wakeup events and the
     * current number of wakeup events being processed is zero, store @count as the
     * old number of registered wakeup events for pm_check_wakeup_events(), enable
     * wakeup events detection and return 'true'.  Otherwise disable wakeup events
     * detection and return 'false'.
     */
    bool pm_save_wakeup_count(unsigned int count)
    {
    	unsigned int cnt, inpr;
    	unsigned long flags;
    
    	events_check_enabled = false;
    	raw_spin_lock_irqsave(&events_lock, flags);
    	split_counters(&cnt, &inpr);
    	if (cnt == count && inpr == 0) {
    		saved_count = count;
    		events_check_enabled = true;
    	}
    	raw_spin_unlock_irqrestore(&events_lock, flags);
    	return events_check_enabled;
    }

    : 여기서 `enabled_check_enabled` 변수는 `wakeup events`를 감지할 지 말지를 결정한다. 예를 들어, 위 코드에서 현재 처리해야 하는 웨이크-업 이벤트가 존재하거나 처리 중인 웨이크-업 이벤트가 있을 경우, `events_check_enabled`는 false를 유지한다. 그리고, 현재 처리해야 하는 웨이크-업 이벤트와 처리 중인 웨이크-업 이벤트가 없을 경우 `events_check_enabled`는 true가 된다. 이 변수는 정리하면 2가지 기능이 의미를 가지고 있다.

    1" 시스템이 suspend 모드로 진입할 지 말지를 결정한다. `events_check_enabled`가 true면, suspend 가능. `events_check_enabled`가 false면, suspend 불가능.

    2" wakeup event를 감지할지 말지를 결정한다. `events_check_enabled`가 false면, 처리해야 하거나 처리 중인 웨이크-업 이벤트가 있음을 의미한다. 즉, 이 시점에서는 웨이크-업 이벤트를 더 탐지하더라도 크게 의미가 없다. 왜냐면, 처리해야 할 웨이크-업 이벤트가 이미 있기 때문이다. `events_check_enabled`가 true면, 처리해야 하거나 처리 중인 웨이크-업 이벤트가 없음을 의미하므로, 웨이크-업 이벤트를 탐지할 수 있어야 한다(suspending에서 웨이크-업을 감지해야 하기 때문이다). 

     

    : `pm_wakeup_pending` 함수는 현재 시스템이 suspend 상태로 진입할 수 있는지 없는지를 반환한다. 즉, 처리 해야 할 웨이크-업 이벤트가 있는지 혹은 처리 중인 웨이크-업 이벤트가 있는지를 판별한다. 그 기준은 아래와 같다.

    1" `registered wakeup event`와 `saved_count`를 비교해서 다르다.
    2" 처리 중인 웨이크-업 이벤트가 존재한다.

    : 위의 2가지 조건 중 하나만 성립되더라도, `pm_wakeup_pending` 함수는 false를 반환한다.

    //drivers/base/power/wakeup.c - v6.5
    /**
     * pm_wakeup_pending - Check if power transition in progress should be aborted.
     *
     * Compare the current number of registered wakeup events with its preserved
     * value from the past and return true if new wakeup events have been registered
     * since the old value was stored.  Also return true if the current number of
     * wakeup events being processed is different from zero.
     */
    bool pm_wakeup_pending(void)
    {
    	unsigned long flags;
    	bool ret = false;
    
    	raw_spin_lock_irqsave(&events_lock, flags);
    	if (events_check_enabled) {
    		unsigned int cnt, inpr;
    
    		split_counters(&cnt, &inpr);
    		ret = (cnt != saved_count || inpr > 0);
    		events_check_enabled = !ret;
    	}
    	raw_spin_unlock_irqrestore(&events_lock, flags);
    
    	if (ret) {
    		pm_pr_dbg("Wakeup pending, aborting suspend\n");
    		pm_print_active_wakeup_sources();
    	}
    
    	return ret || atomic_read(&pm_abort_suspend) > 0;
    }
    EXPORT_SYMBOL_GPL(pm_wakeup_pending);

    : `pm_wakeup_pending`은 suspend 단계에서 여러 번 호출되는 함수다. 즉, suspend 중간중간 `wakeup events`가 있는지를 판별한다. 여기서 또 `events_check_enabled` 변수가 나온다. 이 변수는 위에서 wakeup event를 감지할지 말지를 결정한다고 했다. 그래서 이 값이 true가 될 때, `split_counters` 함수를 호출해서 처리가 완료된 웨이크-업 카운트 개수와 saved_count를 비교해서 wakeup event가 발생했는지를 감지하고 있다. 이 값이 false 라면, wakeup event를 감지하지 않고 `ret || atomic_read(&pm_abort_suspend) > 0 `로 값을 반환한다. 여기서 `pm_abort_suspend`는 뭘까?

     

    : 먼저 `Rafael`이 `pm_abort_suspend`를 도입한 배경에 대해 알 필요가 있다. 리눅스 시스템에서 suspend 진입 시점을 모든 wakeup sources의 deactived를 전제한다. 즉, wakeup source와 시스템 suspend는 강력한 의존 관계를 가지고 있다. 이 말은 wakeup source만이 시스템 suspend를 컨트롤한다는 것인데, 이러한 상황이 상당한 불편함이 있었는지, `Rafael`는 Kernel 3.18 에 wakeup source에 의존하지 않고 독립적으로 시스템 suspend를 중단시킬 수 있는 `pm_abort_suspend` 라는 전역 변수를 추가하게 된다.

    From: Rafael J. Wysocki <rafael.j.wysocki@intel.com>

    It sometimes may be necessary to abort a system suspend in progress or wake up the system from suspend-to-idle even if the pm_wakeup_event()/pm_stay_awake() mechanism is not enabled.

    For this purpose, introduce a new global variable pm_abort_suspend and make pm_wakeup_pending() check its value. Also add routines for manipulating that variable.
    ...

    - 참고 : https://patchwork.kernel.org/project/linux-pm/patch/6935419.nmCT0f1CW7@vostro.rjw.lan/

     

    : 그래서 아래의 패치 코드를 보면, `pm_system_wakeup` 함수에 아무인자도 없는 것을 확인할 수 있다. 이 함수는 `pm_abort_suspend`를 true로 설정한다. 그리고 suspend 단계에서 wakeup event의 여부를 매번 파악하는 `pm_wakeup_pending` 함수에 반환값에 `ret || pm_abort_suspend`를 추가해서 wakeup source와 독립적으로 시스템 suspending을 막을 수 있게 만들었다.

    --- linux-pm.orig/drivers/base/power/wakeup.c
    +++ linux-pm/drivers/base/power/wakeup.c
    @@ -24,6 +24,9 @@ 
      */
     bool events_check_enabled __read_mostly;
     
    +/* If set and the system is suspending, terminate the suspend. */
    +static bool pm_abort_suspend __read_mostly;
    +
     /*
      * Combined counters of registered wakeup events and wakeup events in progress.
      * They need to be modified together atomically, so it's better to use one
    @@ -719,7 +722,18 @@  bool pm_wakeup_pending(void)
     		pm_print_active_wakeup_sources();
     	}
     
    -	return ret;
    +	return ret || pm_abort_suspend;
    +}
    +
    +void pm_system_wakeup(void)
    +{
    +	pm_abort_suspend = true;
    +	freeze_wake();
    +}
    +
    +void pm_wakeup_clear(void)
    +{
    +	pm_abort_suspend = false;
     }
     
     /**
    Index: linux-pm/kernel/power/process.c
    ===================================================================
    --- linux-pm.orig/kernel/power/process.c
    +++ linux-pm/kernel/power/process.c
    @@ -129,6 +129,7 @@  int freeze_processes(void)
     	if (!pm_freezing)
     		atomic_inc(&system_freezing_cnt);
     
    +	pm_wakeup_clear();
     	printk("Freezing user space processes ... ");
     	pm_freezing = true;
     	error = try_to_freeze_tasks(true);

    : 위의 내용까지가 wakeup count의 기본적인 동작 방식이다.

     

    : device 측면에서 wakeup_count는 해당 디바이스와 연관된 wakeu event count를 의미한다.

    The /sys/devices/.../wakeup_count attribute contains the number of signaled wakeup events associated with the device. This attribute is read-only. If the device is not capable to wake up the system from sleep states, this attribute is not present. If the device is not enabled to wake up the system from sleep states, this attribute is empty.

    - 참고 : https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-power

    : `strcut wakeup_source` 에서도 볼 수 있다시피, `/sys/devices/.../wakeup_count`는 해당 디바이스가 시스템 suspend를 몇 번 abort 시켰는지에 대한 카운트값이다.

    //include/linux/pm_wakeup.h - v6.5
    /**
     * struct wakeup_source - Representation of wakeup sources
     *
     * @name: Name of the wakeup source
     * @id: Wakeup source id
     * @entry: Wakeup source list entry
     * @lock: Wakeup source lock
     * @wakeirq: Optional device specific wakeirq
     * @timer: Wakeup timer list
     * @timer_expires: Wakeup timer expiration
     * @total_time: Total time this wakeup source has been active.
     * @max_time: Maximum time this wakeup source has been continuously active.
     * @last_time: Monotonic clock when the wakeup source's was touched last time.
     * @prevent_sleep_time: Total time this source has been preventing autosleep.
     * @event_count: Number of signaled wakeup events.
     * @active_count: Number of times the wakeup source was activated.
     * @relax_count: Number of times the wakeup source was deactivated.
     * @expire_count: Number of times the wakeup source's timeout has expired.
     * @wakeup_count: Number of times the wakeup source might abort suspend.
     * @dev: Struct device for sysfs statistics about the wakeup source.
     * @active: Status of the wakeup source.
     * @autosleep_enabled: Autosleep is active, so update @prevent_sleep_time.
     */
    struct wakeup_source {
    	const char 		*name;
    	int			id;
    	struct list_head	entry;
    	spinlock_t		lock;
    	struct wake_irq		*wakeirq;
    	struct timer_list	timer;
    	unsigned long		timer_expires;
    	ktime_t total_time;
    	ktime_t max_time;
    	ktime_t last_time;
    	ktime_t start_prevent_time;
    	ktime_t prevent_sleep_time;
    	unsigned long		event_count;
    	unsigned long		active_count;
    	unsigned long		relax_count;
    	unsigned long		expire_count;
    	unsigned long		wakeup_count;
    	struct device		*dev;
    	bool			active:1;
    	bool			autosleep_enabled:1;
    };

    : 시스템 측면에서는 registered wakeup events와 wakeup events in progress는 모두 `combined_event_count`에 기록된다. 그런데, 저 시스템 suspend를 막는 것은 `pm_abort_system` 으로 wakeup source 없이도 막을 수 있지만, 일반적으로는 wakable device에 의해서 시스템 suspend가 abort 된다. 이걸 실제 디바이스 측면에서 시스템 suspend를 막을 경우, wakeup count가 어떻게 증가하는지로 알아보자.

     

    : `wakeup_source_report_event` 함수는 PM Core에게 wakeup event가 발생했다는 것을 보고한다. `events_check_enabled`가 true면, `wakeup_count`를 증가시키는 이유가 뭘까? `events_check_enabled`는 suspending 상태에서만 true다. 즉, suspend 상태이거나 혹은 suspend가 아니라면 `events_check_enabled`은 false다. 즉,  `events_check_enabled`가 true라는 것은 현재 상태가 suspend 도중이라는 것을 의미한다. 그러므로, wakeup_count를 증가시킬 수 있는 것이다.

    // drivers/base/power/wakeup.c - v6.5
    /**
     * wakeup_source_activate - Mark given wakeup source as active.
     * @ws: Wakeup source to handle.
     *
     * Update the @ws' statistics and, if @ws has just been activated, notify the PM
     * core of the event by incrementing the counter of the wakeup events being
     * processed.
     */
    static void wakeup_source_activate(struct wakeup_source *ws)
    {
    	unsigned int cec;
    
    	if (WARN_ONCE(wakeup_source_not_registered(ws),
    			"unregistered wakeup source\n"))
    		return;
    
    	ws->active = true;
    	ws->active_count++;
    	ws->last_time = ktime_get();
    	if (ws->autosleep_enabled)
    		ws->start_prevent_time = ws->last_time;
    
    	/* Increment the counter of events in progress. */
    	cec = atomic_inc_return(&combined_event_count);
    
    	trace_wakeup_source_activate(ws->name, cec);
    }
    
    /**
     * wakeup_source_report_event - Report wakeup event using the given source.
     * @ws: Wakeup source to report the event for.
     * @hard: If set, abort suspends in progress and wake up from suspend-to-idle.
     */
    static void wakeup_source_report_event(struct wakeup_source *ws, bool hard)
    {
    	ws->event_count++;
    	/* This is racy, but the counter is approximate anyway. */
    	if (events_check_enabled)
    		ws->wakeup_count++;
    
    	if (!ws->active)
    		wakeup_source_activate(ws);
    
    	if (hard)
    		pm_system_wakeup();
    }

    : `hard`는 즉각적으로 시스템을 wakeup 시키겠다는 뜻이다. `hard`가 false면, 일반적으로는 wakeup event의 알림은 `conbined_event_count` 값을 변경함으로써 PM Core가 알게된다.

     

     

    - Use case

    : 버튼 디바운스 예제를 통해 wakeup count 의 사례를 살펴보자. 먼저, 버튼 디바운스란 버튼이 눌리고 나서 물리적으로 시그널이 안정화되기 까지, 발생한 노이즈를 말한다. 실제로 버튼을 누르면 아래와 같이 `switch bound` 라는 전기적 노이즈가 짧은 순간에 `High ~ Low` 를 왔다갔다 하는 현상이 발생한다.


    https://circuitdigest.com/electronic-circuits/what-is-switch-bouncing-and-how-to-prevent-it-using-debounce-circuit

     

    : 아래의 코드를 보면, 버튼 프레스 인터럽트가 발생하면 딜레이 워크큐를 사용해서 일정 시간뒤에 실제 버튼 동작에 대한 기능을 하는 `gpio_keys_gpio_work_func` 함수가 호출되는 것을 확인할 수 있다. 그런데 만약에, 시스템이 suspend 상태에서 버튼이 눌렸다고 치자. 만약, 버튼이 wakeup interrupt 라면 시스템을 깨울 것이다. 그리고, `gpio_keys_gpio_isr` 함수에서 딜레이 워크를 등록하고 시스템은 다시 suspend 상태로 진입할 것이다. 참고로, 시스템이 wakeup 하고 activa한 wakeup source가 없으면 시스템은 다시 suspend 상태로 들어가려고 한다. 그런데, 시스템 suspend 과정에서 커널은 현재 처리 중인 인터럽트가 있다면, 처리가 모두 끝났을 때 까지 기다려준다. 그래서, `gpio_keys_gpio_isr` 핸들러가 끝날 때 까지는 무리없이 호출될 것 이다. 

    // http://www.wowotech.net/irq_subsystem/irq_handle_procedure.html
    static void gpio_keys_gpio_work_func(struct work_struct *work)
    {
        struct gpio_button_data *bdata =
            container_of(work, struct gpio_button_data, work.work);
     
        gpio_keys_gpio_report_event(bdata);
    }
     
    static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
    {
        struct gpio_button_data *bdata = dev_id;
     
        mod_delayed_work(system_wq,
                 &bdata->work,
                 msecs_to_jiffies(bdata->software_debounce));
     
        return IRQ_HANDLED;
    }

    : 그런데, `gpio_keys_gpio_work_func` 콜백 함수는 언제 실행될 지를 알 수가 없다. 왜냐면, 시스템이 `gpio_keys_gpio_isr` 핸들러가 끝나는 시점에 다시 suspend 상태로 들어가기 때문이다. 추측이지만, 시스템 절전 모드 상태에서는 타이머(RTC)는 tick을 하더라도, 딜레이 워크큐에서 카운트하는 것을 잠시 멈출것이다. 시스템이 resume이 되면, 그 때 다시 딜레이 워크큐의 카운트를 시작할 것이다. 그런데, 이렇게 되면 두 번째 버튼 프레스에서 첫 번째 버튼을 눌렀을 때, 실행 되어야 했을  `gpio_keys_gpio_work_func` 함수가 실행될 가능성있다. 그렇면, `delayed workqueue`가 아닌 `normal workqueue + msleep` 함수를 사용하면 어떨까?

     

    : 이것도 문제가 있다. 일단, `schedule_work` 함수는 즉각적으로 work를 수행하는게 아니다. 프로세스 스케줄링에 의해 선택되어 실행이된다. 그리고, `msleep` 함수는 `mdelay` 함수가 아니기 때문에 다른 프로세스에게 CPU를 양보하게 된다. 즉, 운좋게 `msleep` 까지가더라도, 그 이후에 복귀하지 않고 계속 sleep을 유지할 수 도 있다. 이럴 경우, 다음 wakeup 시에 `gpio_keys_gpio_report_event` 함수가 실행될 수 있는데, 이러면 문제가 커진다.

    // http://www.wowotech.net/irq_subsystem/irq_handle_procedure.html
    static void gpio_keys_gpio_work_func(struct work_struct *work)
    {
        struct gpio_button_data *bdata =
            container_of(work, struct gpio_button_data, work.work);
     
    	msleep(1000);
        gpio_keys_gpio_report_event(bdata);
    }
     
    static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
    {
        struct gpio_button_data *bdata = dev_id;
     
        schedule_work(&bdata->work);
     
        return IRQ_HANDLED;
    }

    : 위와 같은 문제가 발생하는 이유는 리눅스 커널이 워크큐와 같은 프로세스 컨텍스트는 sleep을 허용하지 않기 때문이다(IRQ handler의 실행 컨텍스트는 시스템 sleep이 금지되어 있어서, 매끄러운 실행 환경을 제공해준다). 어떻게 해야 할까? 

     

    : 리눅스 커널에서 시스템이 suspend 상태로 진입할 때, 지속적으로 wakeup count를 확인한다. 왜냐면, 저 값을 통해 wakeup event가 들어왔는지를 확인해서 suspend 를 종료해야 할지를 정해야 하기 때문이다. 이 때, 사용하는 API가 `pm_wakeup_pending` 함수다. `pm_wakeup_pending` 함수는 `combind_event_count` 변수의 값이 증가했으면 suspend를 abort하고, 이전 값(`saved_count`)과 같으면 suspend를 계속 진행한다.

     

    : 이제 해결 방법은 심플하다. 인터럽트가 발생하는 시점에 `pm_stay_awake` 함수를 호출해서 `combind_event_count` 변수의 값을 증가시킨다. 그렇면, 시스템이 suspend 되는 것을 막아준다. 그리고, `gpio_keys_gpio_work_func` 함수가 호출되고 작업을 모두 마무리하면 `pm_relax` 함수를 호출해서 다시 `combind_event_count` 변수의 값을 감소시켜 시스템이 suspend 에 들어갈 수 있도록 해주며 된다. 코드는 아래와 같다.

    // http://www.wowotech.net/irq_subsystem/irq_handle_procedure.html
    static void gpio_keys_gpio_work_func(struct work_struct *work)
    {
        struct gpio_button_data *bdata =
            container_of(work, struct gpio_button_data, work.work);
     
        gpio_keys_gpio_report_event(bdata);
        if (bdata->button->wakeup)
            pm_relax(bdata->input->dev.parent);
    }
     
    static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
    {
        struct gpio_button_data *bdata = dev_id;
     
        if (bdata->button->wakeup) {
            const struct gpio_keys_button *button = bdata->button;
     
            pm_stay_awake(bdata->input->dev.parent);
        }
     
        mod_delayed_work(system_wq,
                 &bdata->work,
                 msecs_to_jiffies(bdata->software_debounce));
     
        return IRQ_HANDLED;
    }
Designed by Tistory.