问题背景:实现一个休眠唤醒的功能,并可控制的使单板进入休眠或者唤醒的状态(管脚拉低进入休眠状态,拉高恢复)。以此来达到LPM(低功耗模式)的目的。
文档以wakeup_in管脚的休眠唤醒功能为例,描述了一个平台驱动从注册,匹配以及Probe内容的填充的一整个驱动实现的流程。
Wakeup_in管脚休眠唤醒功能的调试,大致可以分为下面这几个步骤来实现:
目录
1. 配置设备树
Pinctrl.dtsi配置:
在驱动的注册过程中,设备树起的是信息传递的作用。所以不论是根据驱动找设备树,还是根据设备树找对应的驱动代码,都是可以通过compatible这个属性来查找。
节点名称 {
compatible:compatible属性是一个字符串的列表。(用于驱动和设备的匹配绑定)。wakeup_in_gpio:指向gpio控制器tlmm_pinmux(引用该节点),gpiox_82引脚,低电平有效。qcom,gpio-names:goio名称。
Pinctrl-names:定义了pin脚用到的state列表。
Pinctrl - x:pin脚的两种配置状态。(在驱动中获取pin脚状态时会根据名称来匹配)。
};
GPIO的信息在/sys/kernel/debug/gpio文件内可以查看:
在驱动中获取gpio号时,获取到的不是82,而是一个与Linux通用GPIO API一起使用的GPIO编号,这里是924-1023,其中gpio82对应的GPIO编号是1006。
2. 注册驱动
platfprm_driver结构体如下:
平台驱动结构体内,内嵌继承了一个设备驱动结构体 。后续的注册过程,会完成这个结构体相关内容的填充。
平台驱动的设计主要是完成平台驱动结构体的填充和注册。
以wakeup_in驱动为例,完成了:
平台驱动的probe函数实现:.probe = wakeup_in_probe,。它是驱动的入口函数,当总线完成了设备的 match 操作以后就会进入驱动中该函数的运行。
平台驱动的remove函数实现:.remove= wakeup_in_remove,
实现设备驱动的name、owner、pm、of_match_table变量。
注册的过程:
这里是实现对device_driver结构体内相关函数和变量的赋值。并将 driver的总线类型初始化为platform_bus_type。赋值完成后,将device_driver结构体作为参数传递,继续注册:
device_driver结构体如下:
继续跟代码:
driver_find传入两个参数,name和bus。通过驱动的name在bus上查找这个驱动是否已经注册过了,防止重复注册。(未注册则返回NULL)。
接下来的这个bus_add_driver接口,会根据总线类型把驱动添加到bus上,也是驱动正式开始注册的入口:
先通过bus_get接口获取总线(这里bus_type对应的是platform_bus_type)。
klist_init,kobject_init_and_add 和klist_add_tail,主要完成了Kobject的初始化和将初始化的kobject尾插到kset链表中。并给驱动创建目录(sys/bus/xxx/driver/xxx)。
到这块为止,其实已经将驱动注册到了内核中。接下里的操作,就是尝试给这个驱动匹配对应的设备。
driver_attach:尝试将这个驱动和设备进行绑定。
这里会获取设备链表里的每一个device,并调用fn(dev,data),也就是__driver_attach:
这里的match调用的是platform_match来进行匹配:
先将dev和drv转换为平台设备和驱动,然后会进行一个匹配方式的选择:这里采用OF类型(设备树类型)来进行匹配:
这里的drv->of_match_table,是在平台驱动结构体填充时赋值的:
name/type/compatible这三个匹配项,只要有一个不为空,就满足匹配条件,继续往下执行。
of_device_id结构体:
然后这里会进行一个匹配内容的选择,通过name/type/compatible。可以看出来,compatible是匹配优先级最高的项。这里使用compatible来进行匹配。
如果匹配成功了,则最后返回1。然后回到__driver_attach,执行device_driver_attach:
进入really_probe函数:将device结构中的driver指针指向这个驱动,这里就完成了驱动和设备的匹配绑定。
这里的probe调用的是platform_drv_probe来进行匹配:
接下来会调用上面注册的drv->probe,也就是平台驱动结构体填充时赋值的probe。并且将与驱动匹配的struct platform_device 结构的 dev 作为参数传给drv的probe函数。
至此,驱动的整个注册匹配过程就结束了,接下来就开始probe该驱动。
3. probe函数
定义一个要用到的结构体:
wakeup_in管脚的休眠唤醒,在probe中主要体现在对pin、gpio、irq等资源的申请和初始化,以及相关功能函数的初始化。
调用devm_pinctrl_get函数,向内核申请dev的pin脚资源。成功则会返回一个指向申请到的pin脚号的指针。(一个pinctrl的句柄,后面就可以通过该句柄,来对申请的pin脚进行操作)。
pinctrl_lookup_state这个函数,会根据传入的第二个参数state_name去遍历p->state链表的所有的状态,通过name来进行匹配,如果匹配成功,则返回指向这个状态的指针。
然后通过pinctrl_select_state这个函数,来将设备树中的pinctrl中的信息配置到该pin中。
通过of_get_named_gpio函数,根据第二个参数去设备树中查找对应的gpio,如果查询成功,则返回一个与Linux通用GPIO API一起使用的GPIO编号。
像pin/gpio这些在内核中都属于资源,所以使用之前先申请。通过pinctrl_gpio_request函数来向内核申请一个pin脚当作gpio来使用。
申请到gpio之后,通过pinctrl_gpio_direction_input函数,将gpio配置为输入方向。
通过gpio_to_irq函数,来获取gpio对应的中断号,成功则返回该中断号。
通过gpio_to_irq获取到gpio对应的中断号后,再调用request_irq函数进行中断的注册:将这个中断号、中断的irq_handler、中断触发方式以及中断的名称注册到内核里。(注册的中断可以在/proc/interrupts文件内查看)。
通过wakeup_source_register函数来创建唤醒源设备,并将唤醒源设备加入全局的唤醒源设备列表,成功则返回指向这个唤醒源设备的指针。(注册的唤醒源设备可以在/sys/kernel/debug/wakeup_sources文件内查看)。
调用create_workqueue函数来创建一个工作队列,成功则返回指向该workqueue的指针。
调用INIT_WORK()函数来创建一个工作函数,并将该工作函数赋值给pdata->wk_work变量。
实现驱动LPM模式的suspend和resume,需要有一个中断唤醒源来将系统从sleep状态唤醒。通过调用enable_irq_wake(irq)函数,使该irq具有唤醒系统的功能 。(如果不设置该函数,则进休眠后就不会醒来)。
Probe主要的流程如图所示:
注:中断注册成功之后,要确保该中断有唤醒系统的特性。调用enable_irq_wake()接口来实现该功能。
Probe完成之后,接下来需要填充驱动所要实现的内容:在wakeup_in管脚拉低时,表示允许模组进入休眠状态,拉高时唤醒模组。
Linux pm core提供了wakelock及autoslepp内核休眠机制。autosleep 和 wakelock是并行存在,只有 wakelock 唤醒锁全部释放且 autosleep 为 enable 时,系统才能进入休眠。
autosleep节点路径为/sys/power/autosleep,该值为mem表示打开autoslepp功能,值为off表示关闭。
4. 驱动内容填充
外部触发中断(拉高拉低wakeup_in) --> irq_handler() --> timer_handler() --> work() -->odm_notify()。
拉高或者拉低wakeup_in管脚,触发中断,执行irq_handler:
在中断处理函数中调用mod_timer()来重新启动定时器。
定时时间超时后,执行timer_handle:
在定时器处理函数中,调用queue_work()来将pdata->work工作函数提交到pdata->wk_wq工作队列的工作链表中,并唤醒等待队列,然后执行该工作函数。
通过gpio_get_value()接口获取当前wakeup_in管脚的电平状态,来判断是要进休眠还是要唤醒
如果是要进休眠,则调用_pm_relax()来给wakelock释放锁。
如果是要唤醒,则调用_pm_stay_awake()来给wakelock持锁。这里传入的参数都是probe中申请的指向唤醒源设备的指针。
串口发送awk '{ printf("%-32s %d\n", $1, $6) }' /sys/kernel/debug/wakeup_sources指令可以查看当前设备持锁情况。
要使设备能进休眠或者唤醒,需要满足两个条件:持锁&&autosleep写mem;释放锁&&autosleep写off。
work函数中完成了持锁释放锁的操作,然后在进休眠时调用netlink_report_suspend,唤醒时调用netlink_report_resume函数:
休眠和唤醒对应不同的msg.id,然后调用odm_notify()函数,携带着对应的msg信息传递到上层去处理。
流程框图如图:
5. 上层处理
上层会根据传递上来的msg,根据不同的msg.id来执行对应的case:
如果是要进休眠,则调用frwk_system函数来给autosleep写mem;如果是要唤醒,则给autosleep写off。
至此,休眠和唤醒各自的两个条件都满足了,就可以实现休眠唤醒功能了。