第十九章,电源管理的系统架构与驱动
Linux的电源管理主要包括cpufreq,cpuidle, SMP hotplug, PM QoS, Suspend, Runtime PM等。
CPUfreq程序在drivers/cpufreq目录下,通过cpufreq_register_driver()注册驱动,关键函数是setpolicy()和target()。前者设置频率一个范围,后者直接设置目标频率。用户空间有一个governor,监控当前的负载,调节CPU频率。具体的policy包括ondemand, performance, conservative,powersave, userspace等。设置/sys/devices/system/cpu/cpux/cpufreq下面的governor和setspeed,即可设置策略和频率。CPUFREQ运行时会发出通知,比如CPUFREQ_NOTIFY,CPUFREQ_PRECHANGE,CPUFREQ_POSTCHANGE等。如果某设备需要关注cpufreq的变化,可以调用cpufreq_register_notifier()来接收注册。
除CPUfreq外,Linux也支持Devfreq,见drivers/devfreq目录。
CPUidle的核心是cpu_do_idle() ,一般用汇编实现,ARM中就是WFI。注册驱动使用cpuidle_regsiter_driver,注册CPU使用cpuidle_register_device。结构struct cpuidle_driver的核心是exit_latency(退出延迟)和enter()(进入的方法)。进入IDLE的governor有LADDER和MENU两种,也可以通过sysfs查看和设置。
PM QoS提供了一套接口以设定性能期望,比如开启摄像头时,可以通过pm_qos_add_request设置CPU_DMA_LATENCY,这样cpuidle的governor就会比较目前power state对应的exit latency与QoS的要求,阻止系统进入太深的状态(如C3)。
CPU的热插拔可以通过设置sysfs中的cpux/online状态来实现。在big.LITTLE架构下,Cortex-A15+Cortex-A7, 64位的Cortex-A57+Cortex-A53。两者配合以实现最优功耗。
Linux的挂机可以支持Suspend toRAM, Suspend to Disk等,一般嵌入式系统只实现STR。内核中有一个INPUT_APMPOWER驱动,代码在drivers/input/apm-power.c中,它监听EV_PWR事件,并通过apm_queue_event(APM_USER_SUSPEND)进入STR模式。在各设备驱动的structdevice_driver中,有一个dev_pm_ops的成员,定义了系统Suspend时(进入和退出)的一些回调函数,进入时有prepare, suspend, suspend_late, suspend_noirq,退出时,有resume_noirq, resume_early, resume,complete等。一般来讲,在设备驱动的挂起入口函数中,会关闭设备,关闭该设备的时钟输入,甚至关闭设备的电源,而在恢复时做相反的操作。
将Linux移植到一个新的ARM SoC时,芯片供应商需要实现platform_suspend_ops的成员函数,并用suspend_set_ops()注册。
在系统运行时,某些设备因为没有使用,也可以进入休眠模式,这通过runtime PM实现,在dev_pm_ops中有3个函数runtime_suspend(),runtime_resume(), runtime_idle()。实现的关键是对设备的使用进行计数,函数pm_runtime_get_xxx()增加计数,而pm_runtime_put_xxx()减少计数。增加计数时,设备可能唤醒,调用runtime_resume()。减少计数时,设备可能休眠,调用runtime_idle()。空闲一段时间后,就可能自动调用runtime_suspend()了。,
在消费电子中,可能有超过半数的bug都属于电源管理,很多工作就是在搞定鲁棒性和健壮性。
第二十章,芯片级移植及底层驱动
移植到一个新的SoC上,主要的工作包括定时器驱动、中断控制器、SMP启动、CPU热插拔以及底层的GPIO、时钟、pinctrl和DMA硬件封装等。
当前Linux多支持tickless或者NO_HZ,没有固定的tick。其定时器驱动为clock_event_device和clocksource。前者实现set_mode()和set_next_event()。后者主要是read()函数。在定时器中断中,调用clock_event_device的event_handler()成员函数。在NO_HZ时,使用ONESHOT模式。对于多核处理器,一般是每个核分配一个独立的定时器,各自处理自己的时钟中断。当然也可以只由CPU0处理定时器,然后通过IPI广播到其他核。ARM中1号IPI_TIMER就是负责这个广播的。
中断控制可以在ARM核上,local_irq_enable()/disable()就是直接调用CPSID/CPSIE指令执行。而enable_irq()/disable_irq()针对的是中断控制器。内核中,通过struct irq_chip描述中断控制器。中断号更多的是一个逻辑概念,具体数值不重要。目前多数ARM芯片内部的一级中断控制器都使用了ARM公司的GIC,代码不需要改动,只需要在设备树中添加相关的节点。
在多核系统中,CPU0唤醒其他CPU的动作定义在structsmp_operations中,smp_init_cpus(),然后是smp_boot_secondary()。支持热插拔,实现3个函数cpu_kill(), cpu_die(), cpu_disable()。
为了调试启动过程,需要使能DEBUG_LL和EARLY_PRINTK,实现简单的uart驱动。
GPIO驱动一般在drivers/pinctrl目录下连同pinmux一起实现。
时钟驱动定义于struct clk,其中有clk_ops,关键函数包括set_rate(), set_parent(), get_parent()等。目前一般在DTS中定义时钟树,然后通过clk API来操作所有的时钟。
Dmaengine提供了一套通用的DMA API。首先通过dmaengine_prep_dma_xxx()初始化一个具体的DMA传输描述符。然后调用dmaengine_submit()将该描述符插入到驱动的传输队列。需要传输时,使用dma_async_issue_pending()启动对应DMA通道上的传输。DMA传输完成时,回调函数会被调用。
第二十一章,设备驱动的调试
用户空间的程序可以使用GDB来调试。GDB可以配合图形界面调试工具DDD使用。内核调试可以使能KGDB,然后主机上的GDB可与目标价的KGDB通过串口或网口通信。当然也可以使用JTAG/BDM等硬件调试器。或者使用printk(), oops, strace等。要看到早期的log,需要使能DEBUG_LL和EARLY_PRINTK。
/proc可以用来与内核中实体进行通信。/sys也可以。
系统crash时,会通过Oops给出core dump,这样可以查看事发现场。
调试代码时,可以添加BUG_ON(),WARN_ON()来主动产生core dump,查看函数的call stack。
Strace绑定到一个用户空间的程序(pid)上,可以用来监视系统调用。
如果是调试目标板上的用户程序,可以用GDB server。在目标板上运行GDB server,主机上运行GDB,两者通过串口或者网口通信。在Android上,可以使用adb forward。
在性能优化时,可以使用top, vmstat, iostat, netstat, sar等 ,也可以用sysctl查看所有内核参数或者修改参数。