1.休眠方式 在內核中,休眠方式有很多種,可以通過下麵命令查看 常用的休眠方式有freeze,standby, mem, disk freeze: 凍結I/O設備,將它們置於低功耗狀態,使處理器進入空閑狀態,喚醒最快,耗電比其它standby, mem, disk方式高 standby:除了凍結I/ ...
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\n"); 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\n", error); goto fail2; } error = input_register_device(input); //註冊input按鍵子系統 if (error) { dev_err(dev, "Unable to register input device, error: %d\n", 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=%d\n",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\n",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\n",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=%d\r\n",val); } return 0; }
試驗:
./mykey_text & echo mem > /sys/power/state //然後按GPB31對應的按鍵來喚醒休眠