用最新SDK测试NPU AI Demo用例的时候,发现对NPU的寄存器操作直接导致内核show callstack,打印堆栈显示在调用NPU的初始化CMD (CMD号:KERNEL_CMD_INIT)的时候就失败了。
内核打印堆栈:
可以看到挂在用户态INIT命令的调用栈里面,当等待的中断一直不来时,内核调用gcvip_hang_dump打印NPU DEBUG信息。
出现问题时,连寄存器都读不到:
问题很诡异,如果时钟开了,电开了,不可能连一个chip version只读寄存器都读不到,而且NPU的IO寄存器是通过MEM IO的方式映射的,直接像内存一样读就可以了,怎么会失败呢?
分析整个因果链,会发现一个时钟和电是读取NPU只读寄存器的必要条件,但不一定是充分条件。充分性对分析未知结果的问题有帮助,这个问题我们已经知道结果失败了,那么就需要将主要经历放在分析必要条件是否满足上。
问题原因:
前面已经讲了要分析必要条件,可是必要条件有很多怎么办,逐一分析呗,既然是必要条件,就需要每一条都要满足,任何一条不满足,就可能导致Failure.首先经过分析CCMU配置,排除了时钟因素,NPU的时钟是开启的。那就只剩下电了,事实上,这个问题确实是NPU供电问题导致的。
原因分析:
这个问题和Linux PM domain framework有关,在复杂的片上系统(SOC)中,设计者一般会将系统的供电分为多个独立的block,这称作电源域(Power Domain),这样做可以减少系统供电调节的粒度。同时,这款SOC使用了PPU硬件单元来配合框架实现对IP单独供电的控制。
这种设计引出一个问题:存在多个模块共用一个电源域的情况。因而要求在对模块power on/off的时候,考虑power共用的情况:只要一个模块工作,就要power on;直到所有模块停止工作,才能power off。
Kernel的PM domain framework(位于drivers/base/power/domain.c中),提供了管理和使用系统power domain的统一方法,在解决上面提到的问题的同时,结合kernel的suspend、runtime pm、clock framework等机制,以非常巧妙、灵活的方式,管理系统供电,以达到高效、节能的目的。核心文件首次提交是在2011年:
距离最近的发布版本是Linux-v3.0-rc5:
OK,前序交代完了,这个问题的原因到底是什么呢?原来是在 device tree中添加了PPU的控制pm domain provider的节点,但是并没有在自己的模块,比如这里就是VE,NPU和E907中,添加对应的引用,比如这里就应该在NPU的dts节点中添加:
power-domains = <&pd V853_PD_NPU>;
其中:“power-domains”为pm domain framework的关键字,由framework core负责解析(由名称可知,可以指定多个power domain), “&pd”为provider提供的DTS node名, “V853_PD_NPU”具体的domain标识,也是由provider提供。
但是,实际情况并没有这么做,导致在启动后,PM Core 发现没有模块引用此电源域,这样直接将此电源域的电关闭。
从代码逻辑上分析,平台设备在注册后,在系统启动阶段,解析devicetree的过程中,会通过平台总线调用平台设备的probe函数进行驱动探测:
之后调用genpd_dev_pm_attach将设备和PM provider绑定,可以看到,这里调用了of_parse_phandle_with_args函数,根据当前节点中的“power-domains = <&pd V853_PD_NPU>;”地图,找到对应的provider,获取相应的信息之后,再调用genpd_add_device将设备注册进genpd的链表,不出意外的话,genpd就是pm provider的抽象,也就是上面DTS中pmu节点下面pd子节点的抽象。
这是正常情况下的流程,如果设备不自觉,没有在自己对应的节点上添加上述语句,则会导致of_parse_phandle_with_args执行失败,直接返回 -ENOENT退出。这里的执行失败不会影响到驱动的探测,所以driver->probe继续往下进行,驱动正常加载。
出来混迟早要还的, 虽然歌词上说“谁还没有辜负过几段昂贵的时光”,但是总有些人是没有辜负的。软件也是如此,如果你没有在设备DTS描述节点表中添加类似, power-domains = <&pd V853_PD_NPU>;的语句,你的genpd中就不会对设备进行记录,惩罚很快到来,我们看到同一个文件下,有一个late_initcall函数实现,他就是那位惩罚者,我们看一下它是怎样完成惩罚者的功能的。
首先late_initcall,顾名思义,它是在所有的设备探测结束之后才执行的,先让尘归尘,土归土, 像极了小学生时代班主任准备打人之前先让大家做好,关上门,酝酿情绪,等气氛满足之后才开始动手。这里也是一样,等所有的设备探测结束后,惩罚者开始了它的工作。首先通过上图我们可以看出,系统中可能存在多个pm provider,它们统一记录在一个链表中,这里对链表进行逐一遍历,调用genpd_queue_power_off_work函数进行处理,这个函数也很简单,它仅仅是提交一个工作任务就返回了。
接下来的核心就是genpd_power_off_work_fn函数了:
顺着执行流找到genpd_poweroff函数,这里是实现“惩罚“的关键,我们看到not_suspended变量,他表示同一个genpd电源域中,申请不关闭电源的设备的数量,由于我们前面没有在对应的DTS设备节点描述中增加相应语句,导致311行的对genpd中的设备进行遍历时,设备为0,所以直接bypass了这里。导致not_suspended变量为初始值0.
如果说late_initcall扮演的是惩罚者的角色,那么not_suspended一定就是射出的那颗子弹了,从上图看到,如果not_suspended为0,那么325行的判断会失效,流程继续往下执行,接着,我们就看到了genpd的power_off域函数,即便你不是一个程序员,只要认识这个单词,就知道它将要干什么:
此时,已经没有任何力量可以组织genpd_power_off的执行了,可以看到无论哪个执行流,都会走到power_off函数,就好像这个惩罚者生怕杀不死对方,执法完成后,还要补枪。
至此,这个电源域被关闭了,从此江湖上再也没有NPU的传说。。。
验证:
如果前面的思路正确,只要我能保证GENPD下一个设备请求保持电源,那么NPU应该就能继续工作,由于VE和GPU是同一组,我们用VE做验证,在VE的节点中添加对PM Provider的引用,看能否是能电源域的电量保持。
在VE中添加语句如下,NPU中不动:
经过验证,NPU依旧没有上电,并且not suspended变量仍然是1。
加入打印调试,同时将NPU节点也添加到电源域中来,看到底GENPD是如何分配的,以及各自的设备挂载情况:
打印发现,实际上E907,VE,NPU并不是共享一个Provider,而是各自一个Provider,也就是说,PMU节点定义的是三个Provider,而并非是一个,这是之前的误解。
所以原因可以得到解释了,这样每个provider下都只有一个设备,如果再有一个设备,添加上如下语句:
power-domains = <&pd V853_PD_NPU>;
会增加对NPU Provider的引用,这个时候,有两个设备引用到了NPU的电源域,那么not suspended变量应该就是2了。
比如,我们把VE的电源provider改为NPU,增加如下属性:
所以,果然NPU Provider下有了两个设备节点,都不允许关闭电源。
通过内核debugfs查看更直观,查看/sys/kernel/debug/pm_genpd/pm_genpd_summary节点,可以看到此时有两个驱动挂到了NPU power domain 上,分别是npu和ve.
正常情况下是这样的:
关于power_off和power_on的实现:
总结:
再加入PPU之前,只能通过PMU,或者IP自己的控制寄存器来达到关闭IP电源节省功耗的目的,现在有了PPU,可以将这些通用的上下电管理逻辑放到PPU里面,进行细粒度的集中管理。