zoukankan      html  css  js  c++  java
  • 1.Linux电源管理-休眠与唤醒

    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 state
    ");
                      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: %d
    ",
                              error);
                      goto fail2;
             }
             error = input_register_device(input);   //注册input按键子系统
             if (error) {
                      dev_err(dev, "Unable to register input device, error: %d
    ",
                              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_wakeupdev->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=%d ",err); return -EINVAL; } irq = gpio_to_irq(mykey_data.gpio); //获取IRQ中断号,用来注册中断 if(irq<0) { err =irq; printk("mykey_open err : gpio_to_irq err=%d ",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=%d ",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); //设置定时器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!!!
    ");
         return -1;}
    
     while(1)
     {
         ret=read(fd,&val,1);           //读取一个值,(当在等待队列时,本进程就会进入休眠状态)
         if(ret<0)
         {
         printf("read err!
    ");   
         continue;
         }
      printf("key_val=%d
    ",val);
    }
    close(fd);
    return 0; }

     试验:

    ./mykey_text &              
     echo mem > /sys/power/state      //然后按GPB31对应的按键来唤醒休眠

     

  • 相关阅读:
    maven项目报错:Class path contains multiple SLF4J bindings
    ubuntu18.04 点击启动器实现窗口最小化
    Eclipse lombok get set方法报错
    try-with-resources 让java资源关闭代码更简洁
    yang文件语法格式
    RabbitMQ 交换器、持久化
    RabbitMQ 简介
    systemctl命令配置系统服务
    Karaf基础知识
    Linux shell模拟多线程拷贝
  • 原文地址:https://www.cnblogs.com/lifexy/p/9629699.html
Copyright © 2011-2022 走看看