Search This Blog

Mar 9, 2011

Set Linux power state as "mem" not working

1.客户反馈执行命令 “echo mem > /sys/power/state” 不起作用。

2. 重现和比较:

2.1 我拿客户的板子和程序,编译后跑,可以重现客户说的问题。串口显示
PM: Syncing filesystems ... done.
Freezing user space processes ... (elapsed 0.01 seconds) done.
Freezing remaining freezable tasks ... (elapsed 0.01 seconds) done.
Suspending console(s) (use no_console_suspend to debug)
就不再往下跑了,不过按键盘串口仍然有回显。
2.2 在Linux PC上执行了下“echo mem > /sys/power/state”,PC立即屏幕变黑,按键和鼠标都无反应,按机箱上的Power开关,Linux苏醒过来。
2.3 拿MX53 EVK跑标准程序,执行“echo mem > /sys/power/state”,立即进入suspend,串口无回显,按板子上的Power on键,Linux苏醒过来。

3. 定位最后显示的错误代码

从“Suspending console(s) (use no_console_suspend to debug)”,找到其在文件kernel/printk.c中函数void suspend_console()里面。
void suspend_console(void)
{
 if (!console_suspend_enabled)
  return;
 printk("Suspending console(s) (use no_console_suspend to debug)\n");
 acquire_console_sem();
 console_suspended = 1;
 up(&console_sem);
}
看到一个变量console_suspend_enabled来控制是否使串口进入suspend,往上看一下,这个变量在函数console_suspend_disable()做了赋值,
static int __init console_suspend_disable(char *str)
{
 console_suspend_enabled = 0;
 return 1;
}
__setup("no_console_suspend", console_suspend_disable);
console_suspend_disable()是 _setup()执行的函数,也就是根据u-boot传递过来的启动参数里查看是否定义了“no_console_suspend”,有定义就执行console_suspend_disable()。所有的_setup()圈定的函数会在init/main.c里面的static int __init obsolete_checksetup(char *line)集中执行。
对于调试来说,在函数里面修改比在u-boot参数休改要快捷,于是添加了一行代码在suspend_console()函数里面。
void suspend_console(void)
{
        console_suspend_enabled = 0;
 if (!console_suspend_enabled)
  return;
 printk("Suspending console(s) (use no_console_suspend to debug)\n");
 acquire_console_sem();
 console_suspended = 1;
 up(&console_sem);
}
编译运行后,执行“echo mem > /sys/power/state”打出了大量信息,一个空指针导致kernel崩溃。
root@freescale ~$ echo mem >/sys/power/state
PM: Syncing filesystems ... done.
Freezing user space processes ... (elapsed 0.01 seconds) done.
Freezing remaining freezable tasks ... (elapsed 0.01 seconds) done.
suspend_devices_and_enter
da9052_tsi_suspend: called
PM: suspend of devices complete after 7.109 msecs
mx5_suspend_prepare
suspend wp cpu=400000000
Unable to handle kernel NULL pointer dereference at virtual address 00000008
pgd = 9537c000
[00000008] *pgd=85366031, *pte=00000000, *ppte=00000000
Internal error: Oops: 17 [#1] PREEMPT
last sysfs file: /sys/power/state
Modules linked in:
CPU: 0    Not tainted  (2.6.35.3-744-g27fdf7b-g4ab9a2c-dirty #154)
PC is at set_cpu_freq+0x38/0xe8
LR is at _clk_arm_get_rate+0x20/0x28
pc : [<800541bc>]    lr : [<8003fb74>]    psr: 80000013
sp : 954d7eb8  ip : 00000100  fp : 00000002
r10: 8076fd4a  r9 : 80419028  r8 : 80401bfc
r7 : 00000001  r6 : 3b9aca00  r5 : 17d78400  r4 : 00000000
r3 : 00000001  r2 : 00000000  r1 : 00000004  r0 : 3b9aca00
Flags: Nzcv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment user
Control: 10c5387d  Table: 8537c019  DAC: 00000015
Process sh (pid: 2142, stack limit = 0x954d62e8)
Stack: (0x954d7eb8 to 0x954d8000)
7ea0:                                                       954d7ed0 00000000
7ec0: 8080dde8 00000001 80401bfc 8003c94c 00000000 000f4240 00061a80 003fff00
7ee0: 00000004 00000003 00000000 003fffff 00000004 8008d704 8076fd4a 00000003
7f00: 00000000 8008d930 95364000 00000003 00000003 8008cfd0 950195a0 95454718
7f20: 954d7f80 00000004 95454700 80419028 95002840 801e54d4 950195a0 8010b06c
7f40: 00000004 95404900 2aac9000 954d7f80 00000004 954d6000 0009a26c 800c0ea0
7f60: 95404900 2aac9000 95404900 2aac9000 00000000 00000000 00000004 800c0ff4
7f80: 00000000 00000000 00000004 00000000 00000004 2abf65c8 00000004 00000004
7fa0: 80033104 80032f80 00000004 2abf65c8 00000001 2aac9000 00000004 00000000
7fc0: 00000004 2abf65c8 00000004 00000004 2aac9000 00000000 0009a26c 00000002
7fe0: 00000000 7ed065e0 2ab33c48 2ab8624c 60000010 00000001 70b4a021 70b4a421
[<800541bc>] (set_cpu_freq+0x38/0xe8) from [<8003c94c>] (mx5_suspend_prepare+0xfc/0x13c)
[<8003c94c>] (mx5_suspend_prepare+0xfc/0x13c) from [<8008d704>] (suspend_devices_and_enter+0x94/0x200)
[<8008d704>] (suspend_devices_and_enter+0x94/0x200) from [<8008d930>] (enter_state+0xc0/0x124)
[<8008d930>] (enter_state+0xc0/0x124) from [<8008cfd0>] (state_store+0xa0/0xbc)
[<8008cfd0>] (state_store+0xa0/0xbc) from [<801e54d4>] (kobj_attr_store+0x18/0x1c)
[<801e54d4>] (kobj_attr_store+0x18/0x1c) from [<8010b06c>] (sysfs_write_file+0x108/0x13c)
[<8010b06c>] (sysfs_write_file+0x108/0x13c) from [<800c0ea0>] (vfs_write+0xac/0x154)
[<800c0ea0>] (vfs_write+0xac/0x154) from [<800c0ff4>] (sys_write+0x3c/0x68)
[<800c0ff4>] (sys_write+0x3c/0x68) from [<80032f80>] (ret_fast_syscall+0x0/0x30)
Code: e594203c e1a04003 e5911000 ea000003 (e5920008)
---[ end trace 53b6c4297f475235 ]---

4. 定位空指针

从上面的信息看崩溃的地址在“set_cpu_freq+0x38/0xe8”,本想用工具辅助定位在vmlinux中的具体位置,不过命令都忘了...花了五分钟回忆没有任何结果就改用printk()来定位了,set_cpu_freq这个函数也不大,应该会比较快。函数在arch/arm/plat-mxc/cpufreq.c里面。里面的各个变量,函数都看起来都老实本分,不像是NULL指针应该具有的样子。通常这些冷僻函数久经考验,基本发布后就不会有人修改的。然而现实和理想总是有一定的差距,经过定位cpu_wp_tbl的地址竟然是“0”!
int set_cpu_freq(int freq)
{
...
 pr_info("cpu_wp_tbl addr = 0x%x\n",(unsigned int)cpu_wp_tbl);
 for (i = 0; i < cpu_wp_nr; i++) {
  if (freq == cpu_wp_tbl[i].cpu_rate)
   gp_volt = cpu_wp_tbl[i].cpu_voltage;
 }
...
 return ret;
}

5. 查找cpu_wp_tbl为何为0.

cpu_wp_tbl是什么?经查证,是指CPU working points,保存cpu频率,电压等信息。在cpufreq.c里面函数有对其赋值。
static int __init mxc_cpufreq_driver_init(struct cpufreq_policy *policy)
{
...
  cpu_wp_tbl = get_cpu_wp(&cpu_wp_nr);
 cpu_freq_khz_min = cpu_wp_tbl[0].cpu_rate / 1000;
 cpu_freq_khz_max = cpu_wp_tbl[0].cpu_rate / 1000;
...
}
而get_cpu_wp()是一个指针函数struct cpu_wp *(*get_cpu_wp)(int *wp),在arch/arm/mach-mx5/mx53_wp.c我们找到其赋值的地方.
void mx53_set_cpu_part_number(enum mx53_cpu_part_number part_num)
{
 get_cpu_wp = mx53_get_cpu_wp;
 set_num_cpu_wp = mx53_set_num_cpu_wp;
 switch (part_num) {
 case IMX53_CEC_1_2G:
  cpu_wp_table = cpu_wp_ces_1_2g;
  num_cpu_wp = ARRAY_SIZE(cpu_wp_ces_1_2g);
  break;
 case IMX53_CEC:
  cpu_wp_table = cpu_wp_ces;
  num_cpu_wp = ARRAY_SIZE(cpu_wp_ces);
  break;
 case IMX53_AEC:
 default:
  cpu_wp_table = cpu_wp_aec;
  num_cpu_wp = ARRAY_SIZE(cpu_wp_aec);
  break;
 }
}
那么mx53_get_cpu_wp又做了什么?原来只是返回一个结构数组指针。
struct cpu_wp *mx53_get_cpu_wp(int *wp)
{
 *wp = num_cpu_wp;
 return cpu_wp_table;
}
根据mx53_set_cpu_part_number()函数的内容,cpu_wp_table只能是cpu_wp_ces_1_2g,cpu_wp_ces,cpu_wp_aec。经确认,我们目前跑1GHz,也就是说cpu_wp_table = cpu_wp_ces.看起来逻辑清楚,cpu_wp_tbl怎么会为“0”呢?还是回到cpufreq.c里面的cpu_wp_tbl的赋值部分,看看它拿到一个有效的地址没有。在函数里面加上打印代码:
static int __init mxc_cpufreq_driver_init(struct cpufreq_policy *policy)
{
 int ret;
 int i;

 printk(KERN_INFO "i.MXC CPU frequency driver\n");

 if (policy->cpu != 0)
  return -EINVAL;

 cpu_clk = clk_get(NULL, "cpu_clk");
 if (IS_ERR(cpu_clk)) {
  printk(KERN_ERR "%s: failed to get cpu clock\n", __func__);
  return PTR_ERR(cpu_clk);
 }

 gp_regulator = regulator_get(NULL, gp_reg_id);
 if (IS_ERR(gp_regulator)) {
  clk_put(cpu_clk);
  printk(KERN_ERR "%s: failed to get gp regulator\n", __func__);
  return PTR_ERR(gp_regulator);
 }

 /* Set the current working point. */
 if(get_cpu_wp)
  printk(KERN_INFO"We have get_cpu_wp define!!!!!!!!!!!!!!!!!!!!!!!!!\n");
 else
  printk(KERN_INFO"We have not get_cpu_wp define!!!!!!!!!!!!!!!!!!!!!!!!!\n");
 cpu_wp_tbl = get_cpu_wp(&cpu_wp_nr);
  printk(KERN_INFO"cpu_wp_tbl adddddr= 0x%x\n",(unsigned int)cpu_wp_tbl);

  cpu_freq_khz_min = cpu_wp_tbl[0].cpu_rate / 1000;
 cpu_freq_khz_max = cpu_wp_tbl[0].cpu_rate / 1000;
...
}
编译运行没有跑到加的输出信息那里,也就是说cpu_wp_tbl没有赋值!
仔细查看了启动的输出信息后发现有这样的输出信息:
i.MXC CPU frequency driver
regulator: get() with no identifier
mxc_cpufreq_driver_init: failed to get gp regulator
哦,在cpu_wp_tbl前面出错返回了,没有赋值,所以cpu_wp_tbl为NULL。

6. 为何regulator_get(NULL, gp_reg_id)出错?

这也是一段忠厚老实的代码,为什么总在忠厚老实的代码里面打转?不想质疑regulator_get()可靠性,直接查gp_reg_id。发现其在arch/arm/mach-mx5/bus_freq.c里面定义的,赋值也是在函数busfreq_probe()里面实现的。这里再次强调,一个好的IDE会使查找过程变得非常快速,并有效防止个人在漫长查找过程中思想溜号跑去上网等影响效率的事情发生。
static int __devinit busfreq_probe(struct platform_device *pdev)
{
...
 gp_reg_id = p_bus_freq_data->gp_reg_id;
...
}
恩,再找p_bus_freq_data->gp_reg_id,在arch/arm/mach-mx5/mx53_smd.c发现了定义。
static struct mxc_bus_freq_platform_data bus_freq_data = {
 .gp_reg_id = "DA9052_BUCK_CORE",
 .lp_reg_id = "DA9052_BUCK_PRO",
};
再看发现其竟然没有注册...
// mxc_register_device(&busfreq_device, &bus_freq_data);
迅速打开编译,运行,启动后串口输出没有再打印“mxc_cpufreq_driver_init: failed to get gp regulator”。

7. 验证

在串口键入“echo mem > /sys/power/state”,板子迅速进入suspend,按power on key,板子苏醒。OK,All done!

No comments: