功耗案例分析:周期性底电流抬高问题分析和解决

最近遇到一个间歇性底电流抬高的问题,没有其他提示。

刚开始发现有周期性问题,准备分析中断以及timer,看看能否找到线索。

结果timer数据量太大,timer多,timer超时记录更多;中断也看的云里雾里的。

然后想了一下去看看调度的规律是否能找到问题的根源。

下面就是记录分析的过程。

 

1. 底电流抬高问题描述

问题可以通过下面的一张图来观察。

简单描述就是,每个一段时间底电流就会被抬高0.15mA左右,持续时间不等,然后恢复原样。

功耗案例分析:周期性底电流抬高问题分析和解决

这里有两个问题,一是周期在20+秒左右,另一个是每次底电流抬高的持续时间不等。

 

2. 发现问题根源

首先抓住周期性问题,关键词20秒。尝试通过分析timer、中断等信息,但是数据量很大,找不出头绪。

然后通过分析进程调度,借助trace-cmd和kernelshark查看调度情况。使用方法可以参考:《ftrace利器之trace-cmd和kernelshark》。

2.1 20秒周期问题

通过kernelshark分析如下,可以看出进程1052有着大概的20+秒周期的规律。

功耗案例分析:周期性底电流抬高问题分析和解决

然后分析进程1052的代码,发现这些任务设定在很短的时间结束,周期为20秒。

但是上图中可以看出在20秒超时后,进程被调入,然后又被调出一段时间再调入。这中间持续的时间不等。

2.2 电流抬高时间不等问题

将进程1052的细节展开,可以发现一个周期内工作时间是不一致的。有的持续3.2秒才退出,有的是毫秒级别的。

这就也就解释了为什么电流图中为什么有的持续几秒,有的甚至看不到。

功耗案例分析:周期性底电流抬高问题分析和解决

 

2.3 抬高期间执行情况

可以看出在进程1052的一次执行期间,进入了idle。

中间持续了大概1.86秒,在此期间进程处于操作过程中,导致底电流持续状态过长。

功耗案例分析:周期性底电流抬高问题分析和解决

由以上分析可知,问题的根源在进程1052执行到中途进入了idle线程。

何时唤醒要取决于下一次唤醒时机。

 

2.4 验证假设

简单粗暴的验证就是不启动这个线程,通过修改内核关闭initcall。然后再也发现不了底电流抬高现象。

至此范围已经大幅缩小。

3. 解决方法

3.1 确定问题点msleep()

在基本确定了进程之后,进入进程代码进行分析。

adc_read()

{

  i2c_read();

  i2c_write();

  for(num=1; num<=50; num++)

  {

    i2c_read();

    i2c_write();-----------------------------------------------------(1)

    msleep(5);-----------------------------------------------------(2)

    i2c_read();-----------------------------------------------------(3)

  }

  i2c_read();

  i2c_read();

}

这里需要结合function_graph和function来发现问题。

通过function_graph,监测执行路径上的主要函数,可以动态添加修改。filter可以使用当前进程的pid+函数的组合。

通过function,可以看到每个函数在系统启动timeline上的执行情况。

这两者一结合,最后发现了问题点在(1)和(3)两个函数之间有很大的时间空隙。

所以重点怀疑(2) msleep()。

msleep调度出去,如果系统进入睡眠状态,那么下一次执行的时机就取决于再次唤醒的时间。所以上面表现为底电流抬高时间不定。

将msleep修改为mdelay之后,问题不复存在。

3.2 问题解决方法

使用mdelay()之后问题得到解决,但是带来的副作用也很大。

mdelay()和msleep()的区别在于mdelay()是忙等待,独占CPU。其它进程得不到调度。

在极端情况下,for循环50次,50*5=250ms。这时一个恐怖的延时。

那么最根本的原因是什么呢?

简要分析一下,原来是msleep()之后,当前进程1052调出。系统在无事可做的情况下,进入了idle。

这里的idle是经过特殊处理的,关闭了系统tick的中断响应。

(3)的执行依赖于唤醒中断的时机,这也是抬高电流持续时间不定的根源。

在明白了问题根源之后,也就找到了解决方法。

wake_lock(&adc_wake_lock);

adc_read();

wake_unlock(&adc_wake_lock);

在采取了wake_lock()/wake_unlock()措施之后,问题得到解决。

 

4. 小结

首先根据周期性规律,找到了进程。

然后通过分析进程代码,借助ftrace的function_graph和function两者查看进程中各函数执行流程及其时间戳。

进而找到异常点在(1)和(3)之间,也即msleep()。

由msleep()分析到问题最根本的原因,进而找到解决方法:对关键代码加睡眠锁,读取adc信息期间禁止睡眠。

解决问题。