ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] PM - Driver Power Management
    Linux/kernel 2023. 8. 30. 19:58

    글의 참고

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

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

    - https://www.eefocus.com/article/527386.html

    - https://elinux.org/Pm_Sub_System

     


    글의 전제

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

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


    글의 내용

    - Driver Power Management

    : 리눅스 커널에서 대부분의 장치들은 플랫폼 디바이스라는 소프트웨어적인 가상의 디바이스로 표현된다. 이렇게 하면, 여러 디바이스들을 하나의 가상 디바이스인 `struct platform_device` 로 통일시킴으로써 개발의 편의성을 가져다 준다. 즉, 시스템에 존재하는 각기 다른 디바이스들을 struct platform_device 로 추상화시켜, 통일된 인터페이스를 사용할 수 있게 됨으로써 개발이 편리해진 것 이다. 드라이버 개발자들은 `struct device_driver` 구조체 내부에 존재하는 `shutdown`, `suspend`, `resume` 콜백 함수들을 구현하고 커널에 해당 플랫폼 드라이버를 등록하면, 커널이 전력 관리를 위해 system-wide power transition 과정에서 드라이버가 제공한 PM callback 들을 호출해서 장치의 전원 상태를 변경했었다.

     

    : 그러나, `struct dev_pm_ops` 구조체가 등장하면서, 기존에 `struct device_driver`, `struct bus_type`, `struct class` 구조체 내부에 존재하던 suspend/resume/runtimePM 관련 콜백 함수들이 모두 `struct dev_pm_ops` 구조체 안으로 통합되었다. 그래서 리눅스에서는 `struct device_driver`, `struct bus_type`, `struct class` 구조체 내부에 존재하는 레거시 PM callback 함수 사용을 추천하지 않는다. 그러나, 리눅스 커널 소스를 분석해보면, 여전히 레거시 PM callback 을 호환해주고 있다. 예를 들어, __device_suspend 함수에서 `dev->bus->suspend` 함수를 여전히 지원해주고 있다.

    // drivers/base/power/main.c - v6.5
    static int __device_suspend(struct device *dev, pm_message_t state, bool async)
    {
    	pm_callback_t callback = NULL;
    	const char *info = NULL;
    	int error = 0;
    	....
        
    	if (dev->bus) {
    		if (dev->bus->pm) {
    			info = "bus ";
    			callback = pm_op(dev->bus->pm, state);
    		} else if (dev->bus->suspend) {
    			pm_dev_dbg(dev, state, "legacy bus ");
    			error = legacy_suspend(dev, state, dev->bus->suspend,
    						"legacy bus ");
    			goto End;
    		}
    	}
    	....
        
    	return error;
    }

     

    : 이 글에서는 리눅스 커널의 power transition 과정에서 파워 매니지먼트 서브-시스템 및 드라이버가 어떻게 처리되는지를 다룰 것이다. 드라이버 종류는 많지만, 리눅스에서 대표적으로 사용하는 `platform driver` 를 기준으로 알아보려고 한다. 참고로, 이 글에서 가장 중요한 내용을 언급해놓고 시작해야겠다.

     

    디바이스의 상태를 가장 잘 알고있는건 디바이스와 매핑된 드라이버다.

     

     

    - Old driver power management

    : 기존 리눅스 커널 v2.6 전에는, struct bus_type, struct class, struct device_driver 구조체안에 `shutdown`, `suspend`, `resume` 콜백 함수들이 별도로 존재했다. 그래서 각 계층 별로 별도의 파워 매니지먼트가 필요할 경우, 개별적으로 PM callback 함수를 구현했었다. 예를 들어, `struct bus_type` 안에 `suspend`, `resume` PM callback 함수가 별도로 존재하는 것을 확인할 수 있다.

    // include/linux/device/bus.h - v6.5
    struct bus_type {
    	....
    	void (*remove)(struct device *dev);
    	void (*shutdown)(struct device *dev);
    	....
        
        	int (*suspend)(struct device *dev, pm_message_t state);
    	int (*resume)(struct device *dev);
        	....
    };
    // include/linux/device/driver.h - v6.5
    struct device_driver {
    	....
    	void (*shutdown) (struct device *dev);
    	int (*suspend) (struct device *dev, pm_message_t state);
    	int (*resume) (struct device *dev);
    	....
    };

     

    : 기존 방식은 인터페이스의 구조는 모두 동일하다. 그러나, PM callback 함수들이 각 구조체에 개별적으로 존재했기 때문에 관리의 문제가 있었고, 드라이버와 버스 모두 디바이스라는 측면에서 통일된 인터페이스로 관리될 필요가 있었다. 리눅스 디바이스 모델은 모든 디바이스를 하나의 통일된 디바이스 형태로 바라보면서 새로운 PM 인터페이스인 `struct dev_pm_ops` 구조체를 제시했다.  

     

     

     

    - New driver power management

    : 새로운 DPM에 대해 한 마디로 표현하면 `struct dev_pm_ops`라고 말할 수 있다. `struct dev_pm_ops`에는 굉장히 많은 콜백 함수들이 들어있다. dev_pm_ops 구조체에 대한 자세한 설명은 해당 소스에 코멘트로 자세히 나와있다[링크]. 이 글에서는 `dev_pm_ops`가 전원 전환 프로세스에서 어떤 역할을 하는지에 중점을 둘 것이다.

    //include/linux/pm.h - v6.5
    struct dev_pm_ops {
    	int (*prepare)(struct device *dev);
    	void (*complete)(struct device *dev);
    	int (*suspend)(struct device *dev);
    	int (*resume)(struct device *dev);
    	int (*freeze)(struct device *dev);
    	int (*thaw)(struct device *dev);
    	int (*poweroff)(struct device *dev);
    	int (*restore)(struct device *dev);
    	int (*suspend_late)(struct device *dev);
    	int (*resume_early)(struct device *dev);
    	int (*freeze_late)(struct device *dev);
    	int (*thaw_early)(struct device *dev);
    	int (*poweroff_late)(struct device *dev);
    	int (*restore_early)(struct device *dev);
    	int (*suspend_noirq)(struct device *dev);
    	int (*resume_noirq)(struct device *dev);
    	int (*freeze_noirq)(struct device *dev);
    	int (*thaw_noirq)(struct device *dev);
    	int (*poweroff_noirq)(struct device *dev);
    	int (*restore_noirq)(struct device *dev);
    	int (*runtime_suspend)(struct device *dev);
    	int (*runtime_resume)(struct device *dev);
    	int (*runtime_idle)(struct device *dev);
    };

     

    : dev_pm_ops는 subsystem level 구조체(PM domains, device types, classes, bus types)들 안에 이미 선언되어 있다.

    struct device {
             ...
             struct dev_pm_domain    *pm_domain;
             const struct device_type *type;
             struct class            *class;
             struct bus_type *bus;
             struct device_driver *driver; 
             ...
    };
       
    struct dev_pm_domain {
            struct dev_pm_ops       ops; 
            ...
    };
      
    struct device_type {
            ...
            const struct dev_pm_ops *pm;
    };
      
    struct class {
            ...
            const struct dev_pm_ops *pm;
            ...
    };
      
    struct bus_type {
            ...
            const struct dev_pm_ops *pm;
            ...
    };
     
    struct device_driver {
            ...
            const struct dev_pm_ops *pm;
            ...
    };

    : 커널은 suspend 과정에서 아래의 우선 순위에 따라 `dev_pm_ops`의 콜백 함수를 호출해서 해당 디바이스의 상태를 전환한다. 우선 순위가 존재한다는 것은 우선 순위가 높은 subsystem.dev_pm_ops가 존재하면 그 뒤에 subsystem.dev_pm_ops는 실행하지 않는다는 뜻이다. 

    dev->pm_domain->ops, dev->type->pm, dev->class->pm, dev->bus->pm, dev->driver->pm

     

    : 사실, 일반적인 디바이스 드라이버 개발자라면 `dev->driver->pm` 함수를 구현하는 것에만 관심을 가질 것이다. 그러나, SoC 를 직접 개발하는 회사라면, sub-system 드라이버, 예를 들어 버스 드라이버를 개발하는 경우도 많다. 그리고, class 와 같이 기능별로 디바이스를 분류하는 경우도 sub-system 레벨에서 dev_pm_ops 를 별도로 구현하는 경우가 많다. 예를 들어, `RTC Framework` 또한 `struct class` 구조체를 사용해서 `struct dev_pm_ops`를 구현하고 있다[참고1].

     

    : 아래의 2개의 그림을 이해하는 것은 파워 매니지먼트 계층 구조를 이해하는데 있어 굉장히 중요하다. 아래 그림은 각 sub-system 및 driver 에서 어떤 흐름으로 `struct dev_pm_ops` 콜백 인터페이스가 호출되는지를 나타낸 것이다.


    http://events17.linuxfoundation.org/sites/events/files/slides/kernel_PM_infra_0.pdf

    https://lpc.events/event/4/contributions/284/attachments/220/392/pm_integration.pdf

     

    : 먼저, 위 그림들에서 2가지 내용을 반드시 기억해야 한다.

    1. LDM : PM Domains, Bus, Driver, Device
    2. Non-LDM - Class, Type 

     

    : 먼저 LDM 관점에서 분석한다. 여기서는 3 가지 흐름을 주목해야 한다.

    1. `Action -> Driver Core -> PM domains -> Device`
    2. `Action -> Driver Core -> PM domains -> Bus -> Device`
    3. `Action -> Driver Core -> PM domains -> Bus -> Device Driver -> Device`

     

    : 위 3개의 흐름도는 어떻게 해석할 수 있을까 ? LDM 은 실제 SoC 구조를 정확히 반영하지는 못한다. 왜냐면, 버스와 디바이스를 가지고만 계층 구조를 만들려고 하기 때문이다. 예를 들어, 아래 그림에서 NIC 와 ADC 는 레귤레이터 1(1.8V) 파워 도메인에 묶여 있다. 그리고, Ethernet 은 레귤레이터2(3.3V) 파워 도메인에 묶여 있다. 그런데, 언급된 3개의 디바이스가 Platform Bus 에 묶여있다. 이런 경우, 파워 매니지먼트를 관리하는 개발자 입장에서 어느 것이 더 효율적일까? 실제 하드웨어 구조가 반영된 파워 도메인이 가장 효율적이다. 그렇기 때문에, 파워 도메인에서 struct dev_pm_ops 를 구현했다면 다른 sub-system 들의 dev_pm_ops 를 호출되지 않는다. 

     

    : 그렇다면, 버스와 드라이버 중에는 왜 버스가 우선 순위가 더 높을까? 위에 내용과 같다. 버스는 자신에게 붙어있는 디바이스들을 관리해야 하기 때문에, 외부에서 오는 모든 정보들은 해당 버스 컨트롤러를 거쳐서 디바이스에게 전달되게 된다. 그렇기 때문에, 버스에서 struct dev_pm_ops 를 구현했다면, 드라이버가 구현한 dev_pm_ops 는 호출되지 않는다.

     

    : 그런데, 뭔가 이상하다. 파워 매니지먼트 계층 구조에서는 하위 계층이 먼저 suspend 되고나서, 상위 계층 suspend 되어야 하는데, 왜 sub-system 들이 먼저 dev_pm_ops 를 호출할까? 그 이유는 sub-system 들이 구현하고 있는 dev_pm_ops 는 자신들을 suspend / resume 하는 코드가 아니다. 하위 계층 디바이스를 관리하기 위한 코드들이 대다수이기 때문이다. 그렇다면, sub-system 들은 언제 suspend / resume 될까? 하위 계층 디바이스들이 모두 suspend 될 경우, 상위 계층 디바이스가 suspend 될 준비가 된다. 이 때, 상위 계층 디바이스도 어떤 sub-system 에 속한 디바이스라는 것을 알아야 한다. 아래 그림에서 USB 디바이스들이 모두 suspend 되면, USB Controller 가 suspend 될 준비가 된다. 이 때, USB Controller 는 PCIe 입장에서 PCIe 디바이스일 뿐이다. 그래서, USB Controller 의 suspend 는 PCIe Controller 에 의해서 관리된다.

     

    : 그런데, struct device 구조체는 struct dev_pm_ops 구조체가 선언되어 있지 않은데, 왜 화살표에 최종점에 있는 것일까 ? 모든 sub-system 및 driver 의 struct dev_pm_ops 콜백 인터페이스는 디바이스를 인자로 받기 때문에, 화살표의 최종점이 디바이스가 되는 것이다. 즉, 위에서 디바이스는 신경쓸 필요가 없다.

     

    : Non-LDM 관점에서 분석한다. 여기서는 2 가지 흐름을 주목해야 한다.

    1. `Action -> Driver Core -> Class -> Device`
    2. `Action -> Driver Core -> Type -> Device`

     

    : `class`나 `yype`은 디바이스간의 계층 관계를 나타내지 않는다. 즉, 이 sub-system 들은 누가 `상위`인지 `하위`인지 알 수 가 없다. 파워 매니지먼트는 기본적으로 하위 계층이 먼저 suspend 되고, 그 이후에 상위 계층들이 suspend 된다. 이러한 계층 구조를 `struct class`과 `struct type`은 가지고 있지 않다. 그래서, class & type 은 직접적으로 디바이스 레벨에서 PM callback 을 호출하는 것을 확인할 수 있다. 예를 들어, input sub-system 은 struct class 를 구현한 sub-system 이다. 아래에서 input sub-system 을 사용하는 keyboard, mouse, joystick 의 파워 계층 구조를 알 수 있을까?

     

     

     

     

    - Power domain PM(dev->pm_domain->ops)

    : Power domain 은 가장 우선순위가 높은 PM 계층이다. 파워 도메인은 Runtime PM을 기반으로 하기 때문에, `system suspend` 가 아닌 `runtime suspend` 를 분석해야 한다. `__genpd_runtime_suspend` 함수는 디바이스의 usage count 가 0 이 되면, Runtime 코어에 의해서 호출되는 함수다. 그런데, 제일 먼저 sub-system 레벨에서 `struct dev_pm_ops`를 구현했지 여부를 확인한다. 그리고, 서브 시스템에서 아무도 구현하고 있지 않으면, `dev->-driver->pm` 구현 여부를 판단해서 `.runtime_suspend` 함수를 호출하는 것을 볼 수 있다. 만약, 서브 시스템에서 구현했다면, 드라이버 레벨의 PM 콜백을 호출하지 않는다는 것이다.

    // drivers/base/power/domain.c - v6.5
    /**
     * __genpd_runtime_suspend - walk the hierarchy of ->runtime_suspend() callbacks
     * @dev: Device to handle.
     */
    static int __genpd_runtime_suspend(struct device *dev)
    {
    	int (*cb)(struct device *__dev);
    
    	if (dev->type && dev->type->pm)
    		cb = dev->type->pm->runtime_suspend;
    	else if (dev->class && dev->class->pm)
    		cb = dev->class->pm->runtime_suspend;
    	else if (dev->bus && dev->bus->pm)
    		cb = dev->bus->pm->runtime_suspend;
    	else
    		cb = NULL;
    
    	if (!cb && dev->driver && dev->driver->pm)
    		cb = dev->driver->pm->runtime_suspend;
    
    	return cb ? cb(dev) : 0;
    }
    // drivers/base/power/domain.c - v6.5
    /**
     * genpd_runtime_suspend - Suspend a device belonging to I/O PM domain.
     * @dev: Device to suspend.
     *
     * Carry out a runtime suspend of a device under the assumption that its
     * pm_domain field points to the domain member of an object of type
     * struct generic_pm_domain representing a PM domain consisting of I/O devices.
     */
    static int genpd_runtime_suspend(struct device *dev)
    {
    	....
        
    	int ret;
    	....
    
    	ret = __genpd_runtime_suspend(dev);
    	if (ret)
    		return ret;
    
    	....
        
    	return 0;
    }
    // drivers/base/power/domain.c - v6.5
    int pm_genpd_init(struct generic_pm_domain *genpd,
    		  struct dev_power_governor *gov, bool is_off)
    {
            ....
    	genpd->domain.ops.runtime_suspend = genpd_runtime_suspend;
    	genpd->domain.ops.runtime_resume = genpd_runtime_resume;
            ....
    
    	return 0;
    }

    : 서브-시스템 PM 콜백이 존재하면, 드라이버의 PM 콜백을 호출하지 않는다? 코드상으로도 그렇게 보이지만, 실제로 거의 모든 서브-시스템 PM 콜백 내부에서는 드라이버 PM 콜백을 반드시 호출하는 구조를 갖기 때문에, 너무 걱정할 필요는 없다.

     

     

    - Bus PM(dev->bus->pm)

    : 이번에는 만약, 디바이스가 장착되어 있는 버스에 `dev->bus->pm`이 구현되어 있다면 어떨까? 리눅스 커널에서 디바이스 파워 모델은 항상 전제가 있다. 

     

    특정 디바이스의 상태를 가장 잘 알고있는건 해당 디바이스와 매핑된 드라이버다.

     

     

    : 즉, 버스에 PM 콜백이 구현되어 있더라도, 결국에는 드라이버의 PM 콜백을 최종적으로는 호출하게 되어있다. `platform bus`의 PM 코드를 분석해보자.

    // drivers/base/platform.c - v6.5
    static const struct dev_pm_ops platform_dev_pm_ops = {
    	SET_RUNTIME_PM_OPS(pm_generic_runtime_suspend, pm_generic_runtime_resume, NULL)
    	USE_PLATFORM_PM_SLEEP_OPS
    };
    
    struct bus_type platform_bus_type = {
    	.name		= "platform",
    	.dev_groups	= platform_dev_groups,
    	.match		= platform_match,
    	.uevent		= platform_uevent,
    	.probe		= platform_probe,
    	.remove		= platform_remove,
    	.shutdown	= platform_shutdown,
    	.dma_configure	= platform_dma_configure,
    	.dma_cleanup	= platform_dma_cleanup,
    	.pm		= &platform_dev_pm_ops,
    };
    EXPORT_SYMBOL_GPL(platform_bus_type);
    
    ....
    ....
    
    int __init platform_bus_init(void)
    {
    	int error;
    
    	early_platform_cleanup();
    
    	error = device_register(&platform_bus);
    	if (error) {
    		put_device(&platform_bus);
    		return error;
    	}
    	error =  bus_register(&platform_bus_type);
    	if (error)
    		device_unregister(&platform_bus);
    	of_platform_register_reconfig_notifier();
    	return error;
    }

     

    : `USE_PLATFORM_PM_SLEEP_OPS` 매크로는 다음과 같이 정의되어 있다. 아래 `suspend`에 매핑되는 `platform_pm_suspend` 함수를 따라 가보자.

    // include/linux/platform_device.h - V6.5
    #define USE_PLATFORM_PM_SLEEP_OPS \
    	.suspend = platform_pm_suspend, \
    	.resume = platform_pm_resume, \
    	.freeze = platform_pm_freeze, \
    	.thaw = platform_pm_thaw, \
    	.poweroff = platform_pm_poweroff, \
    	.restore = platform_pm_restore,

     

    : 결국 `platform_pm_suspend` 함수안에서 드라이버의 suspend 함수를 호출한다. 최신 인터페이스(`dev->pm`)을 구현했다면 `drv->pm->suspend` 함수를 호출하고, 구현하지 않았다면 기존 레거시 인터페이스(`pdrv->suspend`)를 호출한다.

    // drivers/base/platform.c - v6.5
    static int platform_legacy_suspend(struct device *dev, pm_message_t mesg)
    {
    	struct platform_driver *pdrv = to_platform_driver(dev->driver);
    	struct platform_device *pdev = to_platform_device(dev);
    	int ret = 0;
    
    	if (dev->driver && pdrv->suspend)
    		ret = pdrv->suspend(pdev, mesg);
    
    	return ret;
    }
    
    ....
    ....
    
    int platform_pm_suspend(struct device *dev)
    {
    	struct device_driver *drv = dev->driver;
    	int ret = 0;
    
    	if (!drv)
    		return 0;
    
    	if (drv->pm) {
    		if (drv->pm->suspend)
    			ret = drv->pm->suspend(dev);
    	} else {
    		ret = platform_legacy_suspend(dev, PMSG_SUSPEND);
    	}
    
    	return ret;
    }

    : 그런데, 플랫폼 버스는 아예 `suspend` 함수를 호출하지 않는 것 같다. 그 이유는 `플랫폼 버스`가 가상의 버스이기 때문이다. 즉, 실제로 존재하는 버스가 아니고, 소프트웨어적으로 모든 하드웨어 버스 인터페이스를 통합시키기 위해 만들어진 가상의 버스다. 그래서, 버스가 존재하지 않을지도 모르는데 디폴트 동작을 정의하기가 애매한 것 이다. 만약, 플랫폼 버스가 실제 물리 버스와 대응한다면 해당 버스에 맞는 suspend 함수를 구현한 뒤, 해당 버스에 등록된 모든 드라이버의 suspend 콜백을 호출함으로써 디폴트 동작과 동일한 구조를 갖게 할 수도 있다.

     

     

    - Driver PM(dev->drv->pm)

    : 드라이버 PM 콜백이 어떻게 호출되는지를 분석할 필요가 있을까? 그럴 필요가 없어졌다. 왜냐면, 나는 이 글을 통해 서브-시스템과 드라이버의 PM 콜백이 어떤 우선순위를 갖고 독립적으로 호출되는지를 알고 싶었다. 그런데, 서브-시스템간의 우선순위는 의미가 있을지 모르지만, 결국 모든 서브-시스템 내부에서 다시 드라이버 PM 콜백을 호출하는 것을 알고나서는 드라이버 PM 콜백을 분석할 필요가 없다고 느꼈다.

     

     

    - Driver practice

    : 이제 실제 PM 기능이 추가된 드라이버를 구현해 볼 것이다. 주로 아래의 내용을 기반으로 구현할 것 이다.

    PM Core" /kernel/power
    PM Function" /driver/base/power

    : `Wakeup source(ws)`는 wakable hardware를 추상시킨 추상화 개념이지만, 그렇다고 `ws`가 반드시 하드웨어에 대응할 필요는 없다. 만약, 드라이버를 개발하는 과정에서 시스템을 wakeup 시켜야 할 일이 있을 때, `ws` 사용하면 된다(드라이버가 반드시 `ws`를 하드웨어 디바이스에 매핑시킬 필요는 없다). 드라이버를 개발하는 입장에서 PM 관련 기능을 구현할 때, 아래의 사항들을 고려해야 한다.

     

    : 모듈이 초기화되는 시점에 `ws`를 생성해서 디바이스와 연결시킨다. 그리고, 드라이버 개발자는 능동적으로 자기 스스로가 시스템이 suspend 되지 말아야 한다고 판단하는 코드다. 즉, `xxx` 함수를 실행하는 시점에는 시스템이 suspend 되지 말아야 한다고 주관적으로 판단한 것이다. 이것이 문제가 뭘까? 일반적으로 디바이스들은 외부에 사용에 의해서만 동작을 한다. 즉, 주체적으로 동작을 하는 경우는 드물다. 특정 목적으로 지속적으로 폴링을 하는 디바이스가 아닌 이상 대부분의 디바이스는 외부의 이벤트를 통해서 동작을 하게 된다. 즉, 외부 이벤트로 인해 디바이스가 동작을 해야 할 경우에만 잠깐 awake 하고 이벤트 처리가 끝나면 다시 suspend 해야 한다는 뜻이다. 근데, `wake lock`은 이러한 원칙을 깨고 유저 애플리케이션에게 시스템을 awake 시킬 막강한 권력을 준 것이다. 당연히 커널 커뮤니티에서는 난리도 아니었다(리눅스 커널이 `arm`을 지원하고, `autosleep`이 추가되면서 이제는 조용해졌다) 

     

    : 결국 사용자가 직접 시스템을 wakeup 시키는 것은 여전히 권장되지 않는 방법이다. 즉, 외부 이벤트로 인해 시스템이 깨어나야 한다. `인터럽트`를 추가해보자(참고로, 아래 코드로는 시스템이 하이버네이션 상태에 있을 경우에는 깨울 수 없다.).

    // http://www.wowotech.net/pm_subsystem/driver_pm.html
    #include <xxx.h>
    ...
    #include <linux/platform_device.h>
    #include <linux/pm_wakeup.h>
     
     
    int xxx(struct device *dev)
    {
    	pm_stay_awake(dev);
    	....
    	pm_relax(dev);
    	...
    }
     
    int xxx_probe(struct platform_device *pdev)
    {
    	struct device *dev = pdev->dev;
    	...
     
    	device_init_wakeup(dev, true);
    	...
    }
     
    int __init xxx_init(void)
    {
    	return platform_driver_register(&xxx_device_driver);
    }
     
    module_initcall(xxx_init);
    MODULE_LICENSE("GPL");

     

    : `request_irq` 함수를 통해 `xxx_isr` 인터럽트 핸들러를 추가한다. 이렇게 하면, `irq` 번호에 대응 뭔가가 트리거되고 시스템이 절전 모드로 되는 것을 막아줄 것 같다.

    // http://www.wowotech.net/pm_subsystem/driver_pm.html
    #include <xxx.h>
    ...
    #include <linux/platform_device.h>
    #include <linux/pm_wakeup.h>
     
    struct device * dev;
     
    int xxx_isr(int irq, void *dev_id)
    {
    	pm_stay_awake(dev);
    	....
    	pm_relax(dev);
    	
    	return IRQ_HANDLED;
    }
     
    int xxx_probe(struct platform_device *pdev)
    {
    	int ret;
    	dev = pdev->dev;
    	...
    	ret = request_irq(irq, xxx_isr, xxx, xxx, xxx);
    	...
    	device_init_wakeup(dev, true);
    	...
    }
     
    int __init xxx_init(void)
    {
    	return platform_driver_register(&xxx_device_driver);
    }
     
    module_initcall(xxx_init);
    MODULE_LICENSE("GPL");

    : 막긴 막는다. 근데, ISR이 실행되고 있을 때만 보장해준다. 내가 원하는 건 시스템이 절전 모드 상태에서 인터럽트를 통해 시스템을 웨이크-업 시키는 것이다. 여기서 뭘 더 해야 할까?

     

    : 어떤 인터럽트가 시스템을 절전 모드에서 깨울 수 있을까? 일반적으로는 아키텍처마다 다르다. 그러나, 대게는 `wakeup function`이라고 설정된 핀들을 통해서 인터럽트가 전달되면 CPU를 깨울 수 있다. 나머지 인터럽트들은 못깨우는 건가? 못깨운다. 왜냐면,  CPU에게 인터럽트가 도달하기 위해서는 `인터럽트 컨트롤러`를 거쳐서 오게된다. 대부분의 일반 인터럽트들은 인터럽트 컨트롤러에서 걸러진다. 데이터 시트에 `wakeup function`이라고 설정된 인터럽트 핀들은 인터럽트 컨트롤러가 절전 모드 상태에서도 CPU 에게 전달한다.

     

    : 아래의 소스에서 추가된 내용은 2가지다.

    IRQF_NO_SUSPEND" 이 플래그가 설정되면, 해당 IRQ는 시스템 suspend / resume 모든 단계에서 계속 enable 상태를 유지한다. 기본적으로, `suspend_noirq`, `non-boot CPU's`단계는 모든 인터럽트 핸들러를 비활성화된 상태다. 그러나, `IRQF_NO_SUSPEND` 플래그가 설정된 인터럽트 핸들러는 이 단계에서도 동작을 한다. 대게, 타이머 인터럽트가 이 플래그가 설정된다. 그러나, 이 플래그가 suspend 단계에서 인터럽트를 계속 활성화시킬 수 는 있지만, 이미 suspended 상태인 시스템을 wakeup 시키는 능력을 가지고 있지는 않다. 이런 경우에는 `enable_irq_wakeup` 함수를 통해 인터럽트를 설정하면 해당 인터럽트를 통해서 suspended 시스템을 깨울 수 있다. 절대, `IRQF_NO_SUSPEND`와 `enable_irq_wakeup` 를 헷갈리면 안된다.

    이 플래그를 `IRQF_SHARED` 플래그와 함께 사용하면, 아주 골치가 아픈 상황이 될 수 있다. 왜냐면, 해당 IRQ에 등록된 모든 인터럽트 핸들러가 NOIRQ 상태에서도 트리거되기 때문이다. 그래서, 기본적으로 `IRQF_NO_SUSPEND`와 `IRQF_SHARED`는 같이 사용되지 않는 것이 좋다.

    enable_irq_wakeup" 위에서도 설명했지만, 실제 suspended 시스템을 wakeup 시키기 위해서는 이 함수를 사용해야 한다. 이 함수의 인자로 전달된 IRQ는 시스템을 wakeup 시키는 기능을 갖게 된다.

    : 아래 구조에서 사실 `IRQF_NO_SUSPEND` 플래그는 자주 사용되는 플래그는 아니다. `IRQF_NO_SUSPEND` 플래그가 설정된 IRQ는 시스템에 정말 중요한 역할을 IRQ 이기 때문에, 일반적으로 포팅되는 디바이스들은 꿈도 못꿀 플래그다.

    // http://www.wowotech.net/pm_subsystem/driver_pm.html
    #include <xxx.h>
    ...
    #include <linux/platform_device.h>
    #include <linux/pm_wakeup.h>
     
    struct device * dev;
     
    int xxx_isr(int irq, void *dev_id)
    {
    	pm_stay_awake(dev);
    	....
    	pm_relax(dev);	
    	return IRQ_HANDLED;
    }
     
    int xxx_probe(struct platform_device *pdev)
    {
    	int ret;
    	int irq;
    	int flag;
     
    	dev = pdev->dev;
    	...
    	ret = request_irq(irq, xxx_isr, flag | IRQF_NO_SUSPEND, xxx, xxx);
    	...
    	enable_irq_wake(irq);
    	device_init_wakeup(dev, true);
    	...
    }
     
    int __init xxx_init(void)
    {
    	return platform_driver_register(&xxx_device_driver);
    }
     
    module_initcall(xxx_init);
    MODULE_LICENSE("GPL");

     

     

    - Driver practice

    : 드라이버를 개발할 때, 파워 매니지먼트 관련해서 몇 가지 주의 사항이 있다.

    1" 디바이스가 시스템을 wakeup 시킬 수 있는 기능이 있다면, `probe` 함수에서 `device_init_wakeup` 함수와 `dev_pm_set_wake_irq` 함수를 차례대로 호출하자(순서 주의). `probe`에 저 2개의 함수를 호출하는 것만으로, 시스템 suspend / resume 시에 자동으로 `enable_irq_wake` / `disable_irq_wake` 함수가 호출된다. `i2c`를 사용하고 있다면 더 좋다. `i2c`는 dts에서 `wakeup-source` 속성을 지원하는데 `i2c-core`가 디바이스 트리를 파싱하면서 자동으로 wakeup 관련 기능을 호출하기 때문이다. 즉, i2c 버스에 부착된 디바이스 드라이버를 개발하고 있다면, `probe`에서 위의 함수들을 호출할 필요가 없다.

    2" 디바이스 드라이버를 개발할 때, suspend / resume 함수를 많이들 작성한다. 그런데, 꼭 suspend 함수안에 인터럽트를 disable 하고, resume 함수안에 다시 인터럽트를 enable 시키는 코드를 작성하곤한다. 그런데, 시스템 suspend / resume 과정을 알고 있는 사람이라면 이런 중복되는 작업은 하지 않을 것이다.

    3" 디바이스 트리르 잘 활용하자. 드라이버를 개발할 때, 마치 예전마냥 `보드 파일`을 작성하듯이 소스 코드에다 하드웨어 정보를 작성하는 경우가 많은데 이러면 유연성이 떨어진다. 하드웨어 부분은 디바이스 트리로, 소프트웨어는 커널 코드로 해서 분리하는 것이 유지 보수에 좋다.

    4" 인터럽트 Bottom-half 에서 워크큐를 사용하고 있다면, `pm_stay_awake`와 `pm_relax` 함수를 쌍으로 이용해야 한다. 만약, 이걸 사용하지 않으면 정확한 동작을 보장하기가 어려워진다.

    'Linux > kernel' 카테고리의 다른 글

    [리눅스 커널] PM - Syscore  (0) 2023.09.01
    [리눅스 커널] PM - Freezing of task  (0) 2023.09.01
    [리눅스 커널] PM - Wakeup source  (0) 2023.08.30
    [리눅스 커널] PM - Wakeup count  (0) 2023.08.29
    [리눅스 커널] PM - Autosleep  (0) 2023.08.07
Designed by Tistory.