Linux电源管理-wakeup events framework

前言

通常新机制/事物的出现往往是解决某些问题的,同样wakeup events framework机制也不例外。先带大家了解下wakeup events framework出现的背景,然后在了解其内部的实现机制。
Linux系统中的电源管理一般是冷睡眠,而Android系统却将linux系统中的睡眠作为通常待机使用,显然Linux中的电源管理不符合Android系统。Android说既然不符合,我就给你改到符合,早期Android就提出了"wakelocks"机制,这种机制将Linux原生的睡眠唤醒流程改变,增加Android自己的处理函数,在一段时间这种机制可以解决Android上的省电,节能问题。但是有一种问题就是suspend和wakeup events之间的同步问题。当系统发生了suspend操作,系统会freeze process,  device prepared, device suspend,disabled irq等,这时候假设有wakeup events产生,而此时系统无法从suspend过程中唤醒。所以Linux在2.6.36中引入了wakeup events framework机制,用来解决suspend和wakeup events之间的同步问题。在Android4.4中,也去掉了之前的"wakelocks"机制,Andoird利用wakeup events framework重新设计了wakelocks,而上层API保持不变。

详细可参考:  http://lwn.net/Articles/388131/    或者https://lwn.net/Articles/416690/

数据结构

wakeup events framework代码在:  /kernel/drivers/base/power/wakeup.c中实现。在wakeup events framework中重要的数据结构就是wakeup_source,字面意思就是产生wakeup events的设备。

  1. /** 
  2.  * struct wakeup_source - Representation of wakeup sources 
  3.  * 
  4.  * @total_time: Total time this wakeup source has been active. 
  5.  * @max_time: Maximum time this wakeup source has been continuously active. 
  6.  * @last_time: Monotonic clock when the wakeup source's was touched last time. 
  7.  * @prevent_sleep_time: Total time this source has been preventing autosleep. 
  8.  * @event_count: Number of signaled wakeup events. 
  9.  * @active_count: Number of times the wakeup source was activated. 
  10.  * @relax_count: Number of times the wakeup source was deactivated. 
  11.  * @expire_count: Number of times the wakeup source's timeout has expired. 
  12.  * @wakeup_count: Number of times the wakeup source might abort suspend. 
  13.  * @active: Status of the wakeup source. 
  14.  * @has_timeout: The wakeup source has been activated with a timeout. 
  15.  */  
  16. struct wakeup_source {  
  17.     const char      *name;  
  18.     struct list_head    entry;  
  19.     spinlock_t      lock;  
  20.     struct timer_list   timer;  
  21.     unsigned long       timer_expires;  
  22.     ktime_t total_time;  
  23.     ktime_t max_time;  
  24.     ktime_t last_time;  
  25.     ktime_t start_prevent_time;  
  26.     ktime_t prevent_sleep_time;  
  27.     unsigned long       event_count;  
  28.     unsigned long       active_count;  
  29.     unsigned long       relax_count;  
  30.     unsigned long       expire_count;  
  31.     unsigned long       wakeup_count;  
  32.     bool            active:1;  
  33.     bool            autosleep_enabled:1;  
  34. };  
.name:    唤醒源的名字。
.entry:     用来将唤醒源挂到链表上,用于管理。
.lock:       同步机制,用于访问链表时使用。
.timer:     定时器,用于设置该唤醒源的超时时间。
.timer_expires:  定时器的超时时间。
.total_time:  wakeup source处于active状态的总时间。
.max_time:  wakeup source处于active状态的最长时间。
.last_time:   wakeup source处于active状态的上次时间。
.start_prevent_time:   wakeup source阻止autosleep的开始时间。
.prevent_sleep_time:  wakeup source阻止autosleep的总时间。
.event_count:  wakeup source上报wakeup event的个数。
.active_count: wakeup source处于active状态的次数。
.relax_count:  wakeup source处于deactive状态的次数。
.expire_count:  wakeup source timeout次数。
.wakeup_count:  wakeup source abort睡眠的次数。
.active:  wakeup source的状态。
.autosleep_enabled:  autosleep使能的状态。

那到底什么是唤醒源呢? 在linux系统中,只有具有唤醒系统的设备才叫做“wakeup source”。 既然只有设备才能唤醒系统,那设备结构体struce device中就应该有某种标志代表此设备是否具有唤醒的能力。
  1. struct device {  
  2.     ...  
  3.     struct dev_pm_info  power;   
  4.     struct dev_pm_domain    *pm_domain;  
  5.     ...  
  6. }  
其中dev_pm_info代表该设备pm相关的详细信息。
  1. struct dev_pm_info {  
  2.     pm_message_t        power_state;  
  3.     unsigned int        can_wakeup:1;  
  4.     unsigned int        async_suspend:1;  
  5.       
  6.     ...  
  7. #ifdef CONFIG_PM_SLEEP  
  8.     struct list_head    entry;  
  9.     struct completion   completion;  
  10.     struct wakeup_source    *wakeup;  
  11.     bool            wakeup_path:1;  
  12.     bool            syscore:1;  
  13. #else  
  14.     unsigned int        should_wakeup:1;  
  15. #endif  
  16.     ...  
  17. }  
其中can_wakeup就代表该设备是否具有唤醒系统的能力。只有具有唤醒能力的device,在sys/devices/xxx/下就会存在power相关目录的。

Sys接口

为了方便查看系统的wakeup sources,linux系统在/sys/kernel/debug下创建了一个"wakeup_sources"文件,此文件记录了系统的唤醒源的详细信息。
  1. static int wakeup_sources_stats_show(struct seq_file *m, void *unused)  
  2. {  
  3.     struct wakeup_source *ws;  
  4.   
  5.     seq_puts(m, "name\t\tactive_count\tevent_count\twakeup_count\t"  
  6.         "expire_count\tactive_since\ttotal_time\tmax_time\t"  
  7.         "last_change\tprevent_suspend_time\n");  
  8.   
  9.     rcu_read_lock();  
  10.     list_for_each_entry_rcu(ws, &wakeup_sources, entry)  
  11.         print_wakeup_source_stats(m, ws);  
  12.     rcu_read_unlock();  
  13.   
  14.     return 0;  
  15. }  
  16.   
  17. static int wakeup_sources_stats_open(struct inode *inode, struct file *file)  
  18. {  
  19.     return single_open(file, wakeup_sources_stats_show, NULL);  
  20. }  
  21.   
  22. static const struct file_operations wakeup_sources_stats_fops = {  
  23.     .owner = THIS_MODULE,  
  24.     .open = wakeup_sources_stats_open,  
  25.     .read = seq_read,  
  26.     .llseek = seq_lseek,  
  27.     .release = single_release,  
  28. };  
  29.   
  30. static int __init wakeup_sources_debugfs_init(void)  
  31. {  
  32.     wakeup_sources_stats_dentry = debugfs_create_file("wakeup_sources",  
  33.             S_IRUGO, NULL, NULL, &wakeup_sources_stats_fops);  
  34.     return 0;  
  35. }  
以下是手机上的wakeup sources信息
  1. root@test:/ # cat /sys/kernel/debug/wakeup_sources                      
  2. name        active_count    event_count wakeup_count    expire_count    active_since    total_time  max_time    last_change prevent_suspend_time  
  3. event1          40644       40644       0       0       0       31294       30      537054822       0  
  4. event4          4496        4496        0       0       0       13369       22      20913677        0  
  5. event5          4496        4496        0       0       0       13048       22      20913677        0  
  6. event0          4540        4540        0       0       0       27995       277     258270184       0  
  7. eventpoll       40688       54176       0       0       0       217     5       537054822       0  
  8. NETLINK         2175        2175        0       0       0       16960       59      537058523       0  
event_count:   代表wakeup source上报wakeup event的个数。
active_count:  当wakeup source产生wakeup events之后,wakup source的状态就处于active。但并不是每次都需要激活该wakup source,如果该wakeup source已经处于激活状态,则就不再需要激活。从一定角度可以说产生该wakup source设备的繁忙程度。
wakeup_count:  当系统在suspend的过程中,如果有wakeup source产生了wakup events事件,就会终止suspend的过程。该变量就记录了终止suspend的次数。

相关API

  • pm_stay_awake(有wakeup events产生后调用此函数通知PMcore)
  1. void pm_stay_awake(struct device *dev)  
  2. {  
  3.     unsigned long flags;  
  4.   
  5.     if (!dev)  
  6.         return;  
  7.   
  8.     spin_lock_irqsave(&dev->power.lock, flags);  
  9.     __pm_stay_awake(dev->power.wakeup);  
  10.     spin_unlock_irqrestore(&dev->power.lock, flags);  
该函数直接就调用__pm_stay_awake函数。此函数可以在中断上下文使用。
  1. void __pm_stay_awake(struct wakeup_source *ws)  
  2. {  
  3.     unsigned long flags;  
  4.   
  5.     if (!ws)  
  6.         return;  
  7.   
  8.     spin_lock_irqsave(&ws->lock, flags);  
  9.   
  10.     wakeup_source_report_event(ws);  
  11.     del_timer(&ws->timer);  
  12.     ws->timer_expires = 0;  
  13.   
  14.     spin_unlock_irqrestore(&ws->lock, flags);  
  15. }  
当wakeup source产生wakup events之后,调用pm_stay_awake函数上报wakeup events。随后会调用pm_relax函数,通知PM core wakeup events已经处理完毕。所以在__pm_stay_awake中不需要定时器。随后调用wakeup_source_report_event上报wakup events。
  1. static void wakeup_source_report_event(struct wakeup_source *ws)  
  2. {  
  3.     ws->event_count++;  
  4.     /* This is racy, but the counter is approximate anyway. */  
  5.     if (events_check_enabled)  
  6.         ws->wakeup_count++;  
  7.   
  8.     if (!ws->active)  
  9.         wakeup_source_activate(ws);  
  10. }  
1.  wakeup events个数加1,也就是event_count加1。
2.  如果events_check_enabled设置了,则会终止系统suspend/hibernate,此时就需要将wakup_count加1, 代表阻止了suspend的次数。
  1. /* 
  2.  * If set, the suspend/hibernate code will abort transitions to a sleep state 
  3.  * if wakeup events are registered during or immediately before the transition. 
  4.  */  
  5. bool events_check_enabled __read_mostly;  
3. 如果wakup source没有激活的,激活该wakup source。假如已经处于active状态,则event_count就比active_count大。
  1. static void wakeup_source_activate(struct wakeup_source *ws)  
  2. {  
  3.     unsigned int cec;  
  4.   
  5.     /* 
  6.      * active wakeup source should bring the system 
  7.      * out of PM_SUSPEND_FREEZE state 
  8.      */  
  9.     freeze_wake();  
  10.   
  11.     ws->active = true;  
  12.     ws->active_count++;  
  13.     ws->last_time = ktime_get();  
  14.     if (ws->autosleep_enabled)  
  15.         ws->start_prevent_time = ws->last_time;  
  16.   
  17.     /* Increment the counter of events in progress. */  
  18.     cec = atomic_inc_return(&combined_event_count);  
  19.   
  20.     trace_wakeup_source_activate(ws->name, cec);  
  21. }  
1.  调用freeze_wake将系统从FREEZE状态唤醒。
2.  更新wakeup source的active的状态。
3.  增加wakeup source的actice_count的引用计数。
4.  设置wakup source的last_time。
5.  如果autosleep enable,设置开始阻止的时间,因为从现在开始就阻止了autosleep。
6.  "wakeup events in progress"加1。"wakeup events in progress"代表系统中有wakeup events正在处理中,不为0,系统不能suspend。
  • pm_relax(唤醒事件处理完毕后,调用该函数通知PM core)
  1. void pm_relax(struct device *dev)  
  2. {  
  3.     unsigned long flags;  
  4.   
  5.     if (!dev)  
  6.         return;  
  7.   
  8.     spin_lock_irqsave(&dev->power.lock, flags);  
  9.     __pm_relax(dev->power.wakeup);  
  10.     spin_unlock_irqrestore(&dev->power.lock, flags);  
  11. }  
该函数也是直接调用__pm_relax函数。
  1. void __pm_relax(struct wakeup_source *ws)  
  2. {  
  3.     unsigned long flags;  
  4.   
  5.     if (!ws)  
  6.         return;  
  7.   
  8.     spin_lock_irqsave(&ws->lock, flags);  
  9.     if (ws->active)  
  10.         wakeup_source_deactivate(ws);  
  11.     spin_unlock_irqrestore(&ws->lock, flags);  
  12. }  
如果该wakeup source已经处于active状态,则调用wakeup_source_deactivate函数deactivce之。
  1. static void wakeup_source_deactivate(struct wakeup_source *ws)  
  2. {  
  3.     unsigned int cnt, inpr, cec;  
  4.     ktime_t duration;  
  5.     ktime_t now;  
  6.   
  7.     ws->relax_count++;  
  8.     /* 
  9.      * __pm_relax() may be called directly or from a timer function. 
  10.      * If it is called directly right after the timer function has been 
  11.      * started, but before the timer function calls __pm_relax(), it is 
  12.      * possible that __pm_stay_awake() will be called in the meantime and 
  13.      * will set ws->active.  Then, ws->active may be cleared immediately 
  14.      * by the __pm_relax() called from the timer function, but in such a 
  15.      * case ws->relax_count will be different from ws->active_count. 
  16.      */  
  17.     if (ws->relax_count != ws->active_count) {  
  18.         ws->relax_count--;  
  19.         return;  
  20.     }  
  21.   
  22.     ws->active = false;  
  23.   
  24.     now = ktime_get();  
  25.     duration = ktime_sub(now, ws->last_time);  
  26.     ws->total_time = ktime_add(ws->total_time, duration);  
  27.     if (ktime_to_ns(duration) > ktime_to_ns(ws->max_time))  
  28.         ws->max_time = duration;  
  29.   
  30.     ws->last_time = now;  
  31.     del_timer(&ws->timer);  
  32.     ws->timer_expires = 0;  
  33.   
  34.     if (ws->autosleep_enabled)  
  35.         update_prevent_sleep_time(ws, now);  
  36.   
  37.     /* 
  38.      * Increment the counter of registered wakeup events and decrement the 
  39.      * couter of wakeup events in progress simultaneously. 
  40.      */  
  41.     cec = atomic_add_return(MAX_IN_PROGRESS, &combined_event_count);  
  42.     trace_wakeup_source_deactivate(ws->name, cec);  
  43.   
  44.     split_counters(&cnt, &inpr);  
  45.     if (!inpr && waitqueue_active(&wakeup_count_wait_queue))  
  46.         wake_up(&wakeup_count_wait_queue);  
  47. }  
1.  wakeup source的deactive状态加1, 也就是relax_count加1。
2.  将wakeup source的状态设置为false。
3.  计算wakeup event处理的时间,然后设置total time,  last_time,  max_time。
4.  如果autosleep使能,更新prevent_sleep_time
  1. static void update_prevent_sleep_time(struct wakeup_source *ws, ktime_t now)  
  2. {  
  3.     ktime_t delta = ktime_sub(now, ws->start_prevent_time);  
  4.     ws->prevent_sleep_time = ktime_add(ws->prevent_sleep_time, delta);  
  5. }  
5.  增加"registered wakeup events"同时减少“wakeup events in progress”。
6.  wakeup count相关的处理,留到wakeup count小节分析。总之简单理解就是激活active的反操作。
  • device_init_wakeup(wakeup source初始化操作,通常在设备驱动中使用该接口)
  1. int device_init_wakeup(struct device *dev, bool enable)  
  2. {  
  3.     int ret = 0;  
  4.   
  5.     if (!dev)  
  6.         return -EINVAL;  
  7.   
  8.     if (enable) {  
  9.         device_set_wakeup_capable(dev, true);  
  10.         ret = device_wakeup_enable(dev);  
  11.     } else {  
  12.         if (dev->power.can_wakeup)  
  13.             device_wakeup_disable(dev);  
  14.   
  15.         device_set_wakeup_capable(dev, false);  
  16.     }  
  17.   
  18.     return ret;  
  19. }  
1.  如果enable等于true,则设置device wakeup capability flag。然后enable wakeup source。
2.  如果enable等于false, 则disable wakeup source, 已经disable device wakeup capability flag。
  • device_set_wakeup_capable(设置device是否有将系统从sleep唤醒的能力)
  1. void device_set_wakeup_capable(struct device *dev, bool capable)  
  2. {  
  3.     if (!!dev->power.can_wakeup == !!capable)  
  4.         return;  
  5.   
  6.     if (device_is_registered(dev) && !list_empty(&dev->power.entry)) {  
  7.         if (capable) {  
  8.             if (wakeup_sysfs_add(dev))  
  9.                 return;  
  10.         } else {  
  11.             wakeup_sysfs_remove(dev);  
  12.         }  
  13.     }  
  14.     dev->power.can_wakeup = capable;  
  15. }  
如果capable等于ture, 则设置power.can_wakup的flag, 然后添加wakup属性到sys中。如果capable等于false,将wakeup属性从sys中移除,然后设置can_wakeup属性。wakup属性定义如下:
  1. static struct attribute *wakeup_attrs[] = {  
  2. #ifdef CONFIG_PM_SLEEP  
  3.     &dev_attr_wakeup.attr,  
  4.     &dev_attr_wakeup_count.attr,  
  5.     &dev_attr_wakeup_active_count.attr,  
  6.     &dev_attr_wakeup_abort_count.attr,  
  7.     &dev_attr_wakeup_expire_count.attr,  
  8.     &dev_attr_wakeup_active.attr,  
  9.     &dev_attr_wakeup_total_time_ms.attr,  
  10.     &dev_attr_wakeup_max_time_ms.attr,  
  11.     &dev_attr_wakeup_last_time_ms.attr,  
  12. #ifdef CONFIG_PM_AUTOSLEEP  
  13.     &dev_attr_wakeup_prevent_sleep_time_ms.attr,  
  14. #endif  
  15. #endif  
  16.     NULL,  
  17. };  
  18. static struct attribute_group pm_wakeup_attr_group = {  
  19.     .name   = power_group_name,  
  20.     .attrs  = wakeup_attrs,  
  21. };  
关于读取/设置wakeup的属性这里不再详细说明,前面已经见过很多次了。
  • device_wakeup_enable(enable device to be a wakeup source)
  1. int device_wakeup_enable(struct device *dev)  
  2. {  
  3.     struct wakeup_source *ws;  
  4.     int ret;  
  5.   
  6.     if (!dev || !dev->power.can_wakeup)  
  7.         return -EINVAL;  
  8.   
  9.     ws = wakeup_source_register(dev_name(dev));  
  10.     if (!ws)  
  11.         return -ENOMEM;  
  12.   
  13.     ret = device_wakeup_attach(dev, ws);  
  14.     if (ret)  
  15.         wakeup_source_unregister(ws);  
  16.   
  17.     return ret;  
  18. }  
1.  如果device不存在或者device不具有wakeup能力,则返回-EINVAL。
2.  创建wakeup source。注册wakeup source。
3.  将设备和wakeup source建立连接。如果失败,则释放wakeup source。
  • wakeup_source_register(分配一个唤醒源,将其加入到wakeup source链表中)
  1. struct wakeup_source *wakeup_source_register(const char *name)  
  2. {  
  3.     struct wakeup_source *ws;  
  4.   
  5.     ws = wakeup_source_create(name);  
  6.     if (ws)  
  7.         wakeup_source_add(ws);  
  8.   
  9.     return ws;  
  10. }  
1.  分配一个wakeup_source结构体,然后设置该wakeup source的name域。
  1. struct wakeup_source *wakeup_source_create(const char *name)  
  2. {  
  3.     struct wakeup_source *ws;  
  4.   
  5.     ws = kmalloc(sizeof(*ws), GFP_KERNEL);  
  6.     if (!ws)  
  7.         return NULL;  
  8.   
  9.     wakeup_source_prepare(ws, name ? kstrdup(name, GFP_KERNEL) : NULL);  
  10.     return ws;  
  11. }  
2. 添加给定的wakeup source到wakup source链表中。
  1. void wakeup_source_add(struct wakeup_source *ws)  
  2. {  
  3.     unsigned long flags;  
  4.   
  5.     if (WARN_ON(!ws))  
  6.         return;  
  7.   
  8.     spin_lock_init(&ws->lock);  
  9.     setup_timer(&ws->timer, pm_wakeup_timer_fn, (unsigned long)ws);  
  10.     ws->active = false;  
  11.     ws->last_time = ktime_get();  
  12.   
  13.     spin_lock_irqsave(&events_lock, flags);  
  14.     list_add_rcu(&ws->entry, &wakeup_sources);  
  15.     spin_unlock_irqrestore(&events_lock, flags);  
  16. }  
其中还包括初始化spinlock,  建立定时器,然后将该wakeup source添加到wakeup_sources链表中。
次数,如果定时器设置的时间超时,则会调用定时器超时函数,在超时函数中deactive wakeup source, 然后超时count加1。
  1. static void pm_wakeup_timer_fn(unsigned long data)  
  2. {  
  3.     struct wakeup_source *ws = (struct wakeup_source *)data;  
  4.     unsigned long flags;  
  5.   
  6.     spin_lock_irqsave(&ws->lock, flags);  
  7.   
  8.     if (ws->active && ws->timer_expires  
  9.         && time_after_eq(jiffies, ws->timer_expires)) {  
  10.         wakeup_source_deactivate(ws);  
  11.         ws->expire_count++;  
  12.     }  
  13.   
  14.     spin_unlock_irqrestore(&ws->lock, flags);  
  15. }  
  • device_wakeup_attach(将wakeup source和device建立连接)
  1. static int device_wakeup_attach(struct device *dev, struct wakeup_source *ws)  
  2. {  
  3.     spin_lock_irq(&dev->power.lock);  
  4.     if (dev->power.wakeup) {  
  5.         spin_unlock_irq(&dev->power.lock);  
  6.         return -EEXIST;  
  7.     }  
  8.     dev->power.wakeup = ws;  
  9.     spin_unlock_irq(&dev->power.lock);  
  10.     return 0;  
  11. }  
如果此设备的wakeup source已经存在,则返回。如果没有则通过传入进来的wakeup source设置。
  • pm_wakeup_event(唤醒wakeup source, 在一段时间之后取消唤醒源)
  1. void __pm_wakeup_event(struct wakeup_source *ws, unsigned int msec)  
  2. {  
  3.     unsigned long flags;  
  4.     unsigned long expires;  
  5.   
  6.     if (!ws)  
  7.         return;  
  8.   
  9.     spin_lock_irqsave(&ws->lock, flags);  
  10.   
  11.     wakeup_source_report_event(ws);  
  12.   
  13.     if (!msec) {  
  14.         wakeup_source_deactivate(ws);  
  15.         goto unlock;  
  16.     }  
  17.   
  18.     expires = jiffies + msecs_to_jiffies(msec);  
  19.     if (!expires)  
  20.         expires = 1;  
  21.   
  22.     if (!ws->timer_expires || time_after(expires, ws->timer_expires)) {  
  23.         mod_timer(&ws->timer, expires);  
  24.         ws->timer_expires = expires;  
  25.     }  
  26.   
  27.  unlock:  
  28.     spin_unlock_irqrestore(&ws->lock, flags);  
  29. }  
首先需要激活wakeup source, 然后如果超时时间是零,则立马又deactive wakeup source, 否则在一段时间之后deactive wakeup source。

示例分析

既然明白了wakeup events framework机制,那驱动程序中应该如何使用呢? 既然不知道如何使用,那就在kernel代码中寻找答案。

1.  一个设备既然要作用唤醒源,必须调用wakeup events framework提供的接口函数,而device_init_wakeup函数就具有此功能,而且还是外部的。在内核中搜索该函数的使用。这时候你会发现有好多处都调用此函数,则就可以顺着此思路探索下去。(kernel/drivers/input/keyboard/gpio-keys.c)

在probe函数中会设置workqueue,  设置timer,  设置wakeup source
  1. INIT_WORK(&bdata->work, gpio_keys_gpio_work_func);  
  2. setup_timer(&bdata->timer,gpio_keys_gpio_timer, (unsigned long)bdata);  
  3. device_init_wakeup(&pdev->dev, wakeup);  

2.  在key按下之后就会调用key的中断处理函数。
  1. static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)  
  2. {  
  3.     ...  
  4.   
  5.     if (bdata->button->wakeup)  
  6.         pm_stay_awake(bdata->input->dev.parent);  
  7.     ...  
  8.       
  9.     return IRQ_HANDLED;  
  10. }  
调用pm_stay_awake通知PM core,有wake events 产生,不能suspend。

3. 在定时器超时函数中调用workqueue, 然后在workqueue出处理按键事件,释放wake events
  1. static void gpio_keys_gpio_work_func(struct work_struct *work)  
  2. {  
  3.     gpio_keys_gpio_report_event(bdata);  
  4.   
  5.     if (bdata->button->wakeup)  
  6.         pm_relax(bdata->input->dev.parent);  
  7.   
  8.      ...  
  9. }  
只不过了每个驱动的调用接口不同罢了。

猜你喜欢

转载自blog.csdn.net/tuyerv/article/details/79816808