首頁 > 軟體

Linux電源管理-休眠與喚醒

2020-06-16 16:46:45

1.休眠方式

 在核心中,休眠方式有很多種,可以通過下面命令檢視

# cat /sys/power/state
          //來得到核心支援哪幾種休眠方式.

常用的休眠方式有freeze,standby, mem, disk

  • freeze:  凍結I/O裝置,將它們置於低功耗狀態,使處理器進入空閒狀態,喚醒最快,耗電比其它standby, mem, disk方式高
  • standby:除了凍結I/O裝置外,還會暫停系統,喚醒較快,耗電比其它 mem, disk方式高
  • mem:      將執行狀態資料存到記憶體,並關閉外設,進入等待模式,喚醒較慢,耗電比disk方式高
  • disk:      將執行狀態資料存到硬碟,然後關機,喚醒最慢

範例:

 # echo standby > /sys/power/state
                  // 命令系統進入standby休眠.

2.喚醒方式

當我們休眠時,如果想喚醒,則需要新增中斷喚醒源,使得在休眠時,這些中斷是設為開啟的,當有中斷來,則會退出喚醒,常見的中斷源有按鍵,USB等.

3.以按鍵驅動為例(基於核心3.10.14)

在核心中,有個input按鍵子系統"gpio-keys"(位於driver/input/keyboard/gpio.keys.c),該平臺驅動platform_driver已經在核心中寫好了(後面會簡單分析)

我們只需要在核心啟動時,註冊"gpio-keys"平台裝置platform_device,即可實現一個按鍵驅動.

3.1首先使板卡支援input按鍵子系統(基於mips君正X1000的板卡)

檢視Makefile,找到driver/input/keyboard/gpio.keys.c需要CONFIG_KEYBOARD_GPIO宏

方式1-修改對應板卡的defconfig檔案,新增宏:

CONFIG_INPUT=y                            //支援input子系統(載入driver/input檔案)
CONFIG_INPUT_KEYBOARD=y             //支援input->keyboards(載入driver/input/keyboard檔案)
CONFIG_KEYBOARD_GPIO=y                  //支援input->keyboards->gpio按鍵(載入gpio.keys.c)

方式2-進入make menuconfig

-> Device Drivers                                                                                                 
  -> Input device support       
    ->  [*]Keyboards 
            [*]  GPIO Buttons

3.2修改好後,接下來寫my_button.c檔案,來註冊platform_device

#include <linux/platform_device.h>
#include <linux/gpio_keys.h>
#include <linux/input.h>

struct gpio_keys_button __attribute__((weak)) board_buttons[] = {
        {
                  .gpio                = GPIO_PB(31),    //按鍵引腳
                  .code  = KEY_POWER,                    //用來定義按鍵產生事件時,要上傳什麼按鍵值
                  .desc                = "power key",    //描述資訊,不填的話會預設設定為"gpio-keys"
                  .wakeup          =1,                  //設定為喚醒源
                  . debounce_interval =10,                //設定按鍵防抖動時間,也可以不設定
                  .type                = EV_KEY,
                  .active_low    = 1,                  //低電平有效
        },
};

static struct gpio_keys_platform_data  board_button_data = {
        .buttons  = board_buttons,
        .nbuttons        = ARRAY_SIZE(board_buttons),
};

struct platform_device  my_button_device = {
        .name              = "gpio-keys",
        .id            = -1,
        .num_resources      = 0,         
        .dev          = {
                .platform_data      = &board_button_data,
        }
};

static int __init button_base_init(void)
{
        platform_device_register(&my_button_device);
        return 0;
}

arch_initcall(button_base_init);   

上面的arch_initcall()表示:

會將button_base_init函數放在核心連結指令碼.initcall3.init段中,然後在核心啟動時,會去讀連結指令碼,然後找到button_base_init()函數,並執行它.

通常,在核心中,platform 裝置的初始化(註冊)用arch_initcall()呼叫

而驅動的註冊則用module_init()呼叫,因為module_init()在arch_initcall()之後才呼叫

因為在init.h中定義:

#define pure_initcall(fn)                  __define_initcall(fn, 0)

#define core_initcall(fn)                  __define_initcall(fn, 1)

#define core_initcall_sync(fn)                __define_initcall(fn, 1s)

#define postcore_initcall(fn)          __define_initcall(fn, 2)

#define postcore_initcall_sync(fn)        __define_initcall(fn, 2s)

#define arch_initcall(fn)        __define_initcall(fn, 3)            // arch_initcall()優先順序為3,比module_init()先執行

#define arch_initcall_sync(fn)                __define_initcall(fn, 3s)

#define subsys_initcall(fn)              __define_initcall(fn, 4)

#define subsys_initcall_sync(fn)    __define_initcall(fn, 4s)

#define fs_initcall(fn)                      __define_initcall(fn, 5)

#define fs_initcall_sync(fn)            __define_initcall(fn, 5s)

#define rootfs_initcall(fn)              __define_initcall(fn, rootfs)

#define device_initcall(fn)              __define_initcall(fn, 6)                    //module_init()優先順序為6

#define device_initcall_sync(fn)    __define_initcall(fn, 6s)

#define late_initcall(fn)          __define_initcall(fn, 7)

#define late_initcall_sync(fn)                  __define_initcall(fn, 7s)

... ...

#define  __initcall(fn)  device_initcall(fn)

#define  module_init(x)  __initcall(fn)         //module_init 等於 device_initcall

3.3然後將my_button.c檔案新增到Makefile中

編譯核心後,便實現一個簡單的按鍵喚醒休眠了.

接下來開始分析platform_driver(位於driver/input/keyboard/gpio.keys.c),看看是如何註冊按鍵和實現喚醒的.

4.分析driver/input/keyboard/gpio.keys.c

4.1該檔案裡有常用的函數有

static int gpio_keys_probe(struct platform_device *pdev);       

設定按鍵和input_dev,註冊input-key子系統

static int gpio_keys_setup_key(struct platform_device *pdev,struct input_dev *input, struct gpio_button_data *bdata,const struct gpio_keys_button *button);

設定GPIO,設定input結構體支援的按鍵值,設定中斷,設定防抖動機制

static irqreturn_t gpio_keys_irq_isr(int irq, void *dev_id);

按鍵中斷函數,如果是中斷源,則通過pm_stay_awake()通知pm子系統喚醒,如果有防抖動,則延時並退出,否則通過schedule_work()來呼叫gpio_keys_gpio_work_func()一次

static void gpio_keys_gpio_timer(unsigned long _data);

定時器超時處理常式,用來實現防抖動,裡面會通過schedule_work()來呼叫一次gpio_keys_gpio_work_func();

static void gpio_keys_gpio_work_func(struct work_struct *work);

處理gpio事件函數,用來上報input事件,並判斷按鍵中斷源,如果是的話,則呼叫pm_relax(),通知pm子系統喚醒工作結束

void pm_wakeup_event(struct device *dev, unsigned int msec);

通知pm(power manager), 喚醒休眠

static int gpio_keys_suspend(struct device *dev);

休眠函數,休眠之前會被呼叫

static int gpio_keys_resume(struct device *dev);

喚醒函數,喚醒之前被呼叫

static SIMPLE_DEV_PM_OPS(gpio_keys_pm_ops, gpio_keys_suspend, gpio_keys_resume);

SIMPLE_DEV_PM_OPS宏位於pm.h,它將會定義一個dev_pm_ops結構體,用來被pm子系統呼叫,實現休眠喚醒

4.2 首先來看probe函數

如下圖所示,probe函數為gpio_keys_probe()

gpio_keys_probe()函數定義如下所示:

static int gpio_keys_probe(struct platform_device *pdev)
{

struct device *dev = &pdev->dev;                          //獲取平台裝置的.dev
const struct gpio_keys_platform_data *pdata = dev_get_platdata(dev);  //獲取my_button.c檔案的board_button_data成員
struct gpio_keys_drvdata *ddata;                          //按鍵驅動資料
const struct gpio_keys_platform_data *pdata = dev_get_platdata(dev);      //獲取平台匯流排裝置資料

if (!pdata) {
              pdata = gpio_keys_get_devtree_pdata(dev);
                  if (IS_ERR(pdata))
                          return PTR_ERR(pdata);}

ddata = kzalloc(sizeof(struct gpio_keys_drvdata) +
                          pdata->nbuttons * sizeof(struct gpio_button_data),
                          GFP_KERNEL);                                          //給平台裝置資料分配空間

input = input_allocate_device();                        //分配input 按鍵子系統
        if (!ddata || !input) {
                  dev_err(dev, "failed to allocate staten");
                  error = -ENOMEM;
                  goto fail1;
        }

ddata->pdata = pdata;                                   
ddata->input = input;
mutex_init(&ddata->disable_lock);

platform_set_drvdata(pdev, ddata);           
//將ddata儲存到平台匯流排裝置的私有資料。以後只需呼叫platform_get_drvdata()就能獲取驅動資料
//設定pdev->dev->p結構體成員下的資料
//令pdev->dev->p->device = &pdev->dev
//pdev-> dev->p->driver_data = ddata

input_set_drvdata(input, ddata);
//將ddata儲存到要註冊的按鍵子系統驅動的私有資料中。以後只需呼叫input_get_drvdata()就能獲取驅動資料
//設定input ->dev->p結構體成員下的資料
//令input ->dev->p->device = &pdev->dev
// input ->p->driver_data = ddata

        input->name = pdata->name ? : pdev->name;          //等於"gpio-keys"
        input->phys = "gpio-keys/input0";          //input 按鍵子系統處於/sys目錄下的哪個路徑
        input->dev.parent = &pdev->dev;
        input->open = gpio_keys_open;            //input開啟操作,用來正常喚醒呼叫
        input->close = gpio_keys_close;            //input關閉操作,用來正常休眠呼叫

        input->id.bustype = BUS_HOST;            //設定匯流排
        input->id.vendor = 0x0001;
        input->id.product = 0x0001;
        input->id.version = 0x0100;

        /* Enable auto repeat feature of Linux input subsystem */
        if (pdata->rep)
        __set_bit(EV_REP, input->evbit);     
        for (i = 0; i < pdata->nbuttons; i++) {
                  const struct gpio_keys_button *button = &pdata->buttons[i];  //獲取每個按鍵
                  struct gpio_button_data *bdata = &ddata->data[i];

                  error = gpio_keys_setup_key(pdev, input, bdata, button);// gpio_keys_setup_key()裡會執行:
                          //賦值按鍵,使 bdata->button = button
                            //通過gpio_request_one()來申請button->gpio管腳為輸入模式
                          //判斷 button->debounce_interval成員,是否設定防抖動時間
                          //獲取按鍵對應的中斷號,並賦值給bdata->irq
                            //通過__set_bit()來讓input 按鍵子系統支援button->code
                          //通過setup_timer()設定bdata->timer結構體對應的超時函數
                          //通過request_any_context_irq()函數註冊按鍵中斷:
                              // ----> 中斷號為bdata->irq,中斷名叫: button.desc("power key")
                              // ----> 中斷標誌位為(IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
                              // ----> 中斷服務函數為gpio_keys_gpio_isr(),設定中斷函數引數dev_id為bdata

                  if (error)
                          goto fail2;

                  if (button->wakeup)                //判斷該按鍵是否是喚醒源
                          wakeup = 1;           
        }

        error = sysfs_create_group(&pdev->dev.kobj, &gpio_keys_attr_group);  //在/sys/devices/platform/gpio-keys下建立sys裝置節點
        if (error) {
                  dev_err(dev, "Unable to export keys/switches, error: %dn",
                          error);
                  goto fail2;
        }
        error = input_register_device(input);  //註冊input按鍵子系統
        if (error) {
                  dev_err(dev, "Unable to register input device, error: %dn",
                          error);
                  goto fail3;
        }
        device_init_wakeup(&pdev->dev, wakeup);        //如果按鍵有設定喚醒源,則設定標誌位
}

4.3其中device_init_wakeup()函數定義如下:

static inline int device_init_wakeup(struct device *dev, bool val)
{
        device_set_wakeup_capable(dev, val);  //設定dev->power.can_wakeup = val;
        device_set_wakeup_enable(dev, val);  //設定dev->power.should_wakeup = val;
        return 0;
}

然後休眠喚醒的時候,就會根據dev->power.can_wakeup和dev->power.should_wakeup來做不同的操作

4.4 其中gpio_keys_suspend()休眠函數定義如下所示:

static int gpio_keys_suspend(struct device *dev)
{
        struct gpio_keys_drvdata *ddata = dev_get_drvdata(dev);
        struct input_dev *input = ddata->input;
        int i;

        if (device_may_wakeup(dev)) //判斷dev->power.can_wakeup和dev->power.should_wakeup,是否有中斷源按鍵
      {             
                  for (i = 0; i < ddata->pdata->nbuttons; i++) //如果有,則遍歷每個按鍵
          {
                          struct gpio_button_data *bdata = &ddata->data[i];
                          if (bdata->button->wakeup)
                                  enable_irq_wake(bdata->irq);  //將要睡眠的中斷號遮蔽掉,實現休眠時保持中斷喚醒
                }
        }
      else
      {
                  mutex_lock(&input->mutex);
                  if (input->users)
                          gpio_keys_close(input); //呼叫dev->platform_data-> disable成員函數
                  mutex_unlock(&input->mutex);
        }
        return 0;
}

從上面函數可以看到,進入休眠之前,我們需要呼叫enable_irq_wake()來設定喚醒源

4.5 然後在中斷函數中,判斷是否需要上報喚醒事件,中斷函數如下所示:

static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
{
        struct gpio_button_data *bdata = dev_id;
        BUG_ON(irq != bdata->irq);

        if (bdata->button->wakeup)
                  pm_stay_awake(bdata->input->dev.parent);      //如果是喚醒源,則通知pm子系統,處理喚醒事件,並等待結束

        if (bdata->timer_debounce)
                  mod_timer(&bdata->timer,jiffies + msecs_to_jiffies(bdata->timer_debounce));//如果設定防抖動,則啟動定時器並退出

        else
                  schedule_work(&bdata->work);  //否則呼叫bdata->work對應的函數gpio_keys_gpio_work_func()
        return IRQ_HANDLED;
}

其中gpio_keys_gpio_work_func()函數如下所示:

static void gpio_keys_gpio_work_func(struct work_struct *work)
{
        struct gpio_button_data *bdata= container_of(work, struct gpio_button_data, work); 

        gpio_keys_gpio_report_event(bdata); //上傳input按鍵事件
       
     if (bdata->button->wakeup)
      pm_relax(bdata->input->dev.parent);      //如果是喚醒源,則通知pm子系統,喚醒中斷處理結束。
}

從上面兩個函數可以看到,喚醒休眠時,需要使用兩個函數實現:

pm_stay_awake();                  //在中斷入口呼叫,告知啟動喚醒
pm_relax();                        //在中斷出口呼叫,告知結束喚醒

在中斷前呼叫pm_stay_awake(),中斷結束時再呼叫一次pm_relax()函數.

4.6 如果想延時喚醒,也可以使用另一種喚醒休眠,則只需要一個函數實現:

pm_wakeup_event(struct device *dev, unsigned int msec);

        //通知pm子系統在msec後處理喚醒事件, msec=0,則表示立即喚醒

4.7 接下來來看gpio_keys_setup_key(),如何設定按鍵的(只加了重要的部分)

static int gpio_keys_setup_key(struct platform_device *pdev,
                                  struct input_dev *input,
                                  struct gpio_button_data *bdata,
                                  const struct gpio_keys_button *button)
{
        const char *desc = button->desc ? button->desc : "gpio_keys"; //獲取平台裝置設定的名字
        //… …
        error = gpio_request_one(button->gpio, GPIOF_IN, desc);//申請button->gpio引腳,並將引腳設為輸入引腳,名字設定為desc

        if (button->debounce_interval)   
        {
        bdata->timer_debounce =button->debounce_interval;      //設定防抖動時間
      }  
      irq = gpio_to_irq(button->gpio);                    //獲取管腳對應的中斷號  
      if (irq < 0)
      {  
            //… …  
           goto fail;  
      }

     bdata->irq = irq;

  INIT_WORK(&bdata->work, gpio_keys_gpio_work_func);  
  //初始化bdata->work,使bdata->work與gpio_keys_gpio_work_func()函數關聯起來  
  //後面當呼叫schedule_work(&bdata->work)時,便會執行gpio_keys_gpio_work_func()函數
    
  setup_timer(&bdata->timer, gpio_keys_gpio_timer, (unsigned long)bdata);  
        //設定gpio_keys_gpio_timer()定時器超時函數,用來實現防抖動,函數引數為bdata

  irqflags = IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING;  
                          //中斷標誌位

  isr = gpio_keys_gpio_isr;                       

  input_set_capability(input, button->type ?: EV_KEY, button->code); //使input 支援EV_KEY鍵盤事件,並使鍵盤事件支援button->code按鍵值
 
    error = request_any_context_irq(bdata->irq, isr, irqflags, desc, bdata);  
          //通過request_any_context_irq()函數註冊按鍵中斷:  
      //中斷號為bdata->irq,中斷名叫: button.desc("power key")  
      //中斷標誌位為(IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)  
      //中斷服務函數為gpio_keys_gpio_isr(),設定中斷函數引數dev_id為bdata

        return 0;
  
}

通過gpio.keys.c,得出喚醒流程:

休眠時:

enable_irq_wake (bdata->irq);           
//將要睡眠的中斷號遮蔽掉,實現休眠時保持中斷喚醒

喚醒後:

disable_irq_wake(bdata->irq);      //關閉喚醒

中斷時,有兩種喚醒PM模式

模式1-使用兩個函數實現:
•進入中斷時呼叫一次pm_stay_awake().
•退出時也呼叫一次pm_relax(bdata->input->dev.parent);

模式2-只需一個函數實現:
•進入中斷時呼叫pm_wakeup_event(struct device *dev, unsigned int msec).

5.接下來,我們自己寫個???鍵字元驅動,實現休眠喚醒

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/slab.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/gpio_keys.h>
#include <linux/workqueue.h>
#include <linux/gpio.h>
#include <linux/of_platform.h>
#include <linux/of_gpio.h>
#include <linux/spinlock.h>
#include <soc/gpio.h>


#define MYKEY_GPIO GPIO_PB(31)
static DECLARE_WAIT_QUEUE_HEAD(mykey_waitqueue);

struct mykey_button {
        unsigned int gpio;         
        const char *desc;
        int wakeup;                                /*喚醒源*/           
        int debounce_interval;                    /* 防抖動 時間ms*/
        int wait_event;                            /*等待佇列事件*/
        int key_val;                              /*按鍵值*/
        int irq;
        struct timer_list timer;                  /*防抖動定時器*/
        struct work_struct work;
        struct device *dev;
};


static struct mykey_button mykey_data={
                  .gpio = MYKEY_GPIO,
                  .desc = "mykey",
                  .wakeup = 1,
                  .debounce_interval = 10,                //10ms
                  .wait_event = 0,
};

static void mykey_func(struct work_struct *work)
{

        struct mykey_button *data = container_of(work, struct mykey_button, work);  //通過work成員變數找到父結構體

        if(data->wakeup)
        {
                  pm_wakeup_event(data->dev, 0);
        }

        data->key_val =gpio_get_value(data->gpio);
        data->wait_event =1;
        wake_up_interruptible(&mykey_waitqueue);                        //喚醒佇列
}

static void mykey_irq_timer(unsigned long _data)
{
        struct mykey_button *data  =(struct mykey_button *)_data;
        schedule_work(&data->work);                                        //呼叫mykey_func()函數
}

static irqreturn_t  mykey_irq(int irq, void *dev_id)
{
        struct mykey_button *data =  dev_id;

        if(data->debounce_interval)
                  mod_timer(&data->timer, jiffies+msecs_to_jiffies(10));
        else
                  schedule_work(&data->work);

        return IRQ_HANDLED;
}

static ssize_t  mykey_read(struct file *file, char __user *user, size_t count, loff_t *ppos)
{
        wait_event_interruptible(mykey_waitqueue,mykey_data.wait_event );            //進入等待佇列休眠,如果中斷來資料,則跳出

        copy_to_user(user, &mykey_data.key_val, sizeof(mykey_data.key_val));

        mykey_data.wait_event =0;

        return 0;
}

static int mykey_open(struct inode *inode, struct file *file)
{
        int err=0;
        int irq;
        err=gpio_request_one(mykey_data.gpio, GPIOF_DIR_IN,  mykey_data.desc);                //獲取管腳,並設定管腳為輸入
        if (err < 0) {
                  printk("mykey_open err : gpio_request_one  err=%dn",err);
                  return -EINVAL;
        }

        irq = gpio_to_irq(mykey_data.gpio);                      //獲取IRQ中斷號,用來註冊中斷
        if(irq<0)
        {
                  err =irq;
                  printk("mykey_open err : gpio_to_irq err=%dn",irq);
                  goto fail;
        }
        mykey_data.irq = irq;
        INIT_WORK(&mykey_data.work, mykey_func);            //初始化工作佇列

        err=request_irq(irq,mykey_irq,IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING, mykey_data.desc,&mykey_data);
        if (err) {
                  printk("mykey_open err : request_irq err=%dn",err);
                  goto fail;
        }
        if(mykey_data.wakeup)
                  enable_irq_wake(irq);                      //將引腳設為喚醒源

        if(mykey_data.debounce_interval)
                  setup_timer(&mykey_data.timer, mykey_irq_timer, (unsigned long)&mykey_data);  //設定定時器
        add_timer(&mykey_data.timer);

        return 0;
fail:
        if (gpio_is_valid(mykey_data.gpio))
                  gpio_free(mykey_data.gpio);

        return err;
}

static int mykey_release(struct inode *inode, struct file *file)
{     
        free_irq(mykey_data.irq,&mykey_data);   
        cancel_work_sync(&mykey_data.work);
        if(mykey_data.wakeup)
                  disable_irq_wake(mykey_data.irq);
        if(mykey_data.debounce_interval)
                  del_timer_sync(&mykey_data.timer);
        gpio_free(mykey_data.gpio);
        return 0;
}

struct file_operations mykey_ops={
        .owner  = THIS_MODULE,
        .open    = mykey_open,
        .read    =  mykey_read,
        .release=mykey_release,
};

static int major;
static struct class *cls;
static int mykey_init(void)
{
        struct device *mydev; 
        major=register_chrdev(0,"mykey", &mykey_ops);
        cls=class_create(THIS_MODULE, "mykey");
        mydev = device_create(cls, 0, MKDEV(major,0),&mykey_data,"mykey");
        mykey_data.dev = mydev;
        return 0;
}

static void mykey_exit(void)
{
        device_destroy(cls, MKDEV(major,0));
        class_destroy(cls);
        unregister_chrdev(major, "mykey");
}

module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");

應用測試程式碼如下:

#include <sys/types.h> 
#include <sys/stat.h>   
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

int main(int argc,char **argv)
{
 int fd,ret;
 unsigned int val=0;             
 fd=open("/dev/mykey",O_RDWR);     
 if(fd<0)
    {printf("can't open!!!n");
    return -1;}

 while(1)
 {
    ret=read(fd,&val,1);          //讀取一個值,(當在等待佇列時,本進程就會進入休眠狀態)
    if(ret<0)
    {
    printf("read err!n"); 
    continue;
    }
  printf("key_val=%drn",val);
}
 return 0;
}

 試驗:

./mykey_text &              
 echo mem > /sys/power/state      //然後按GPB31對應的按鍵來喚醒休眠

Linux公社的RSS地址:https://www.linuxidc.com/rssFeed.aspx

本文永久更新連結地址https://www.linuxidc.com/Linux/2018-09/154026.htm


IT145.com E-mail:sddin#qq.com