Power management primer : How to write a cpu frequency controlling driver?
You must be familiar with dynamically changing cpu frequencies. On x86 this has been a de-facto standard for a long time. But many of the embedded systems, where the importance of dynamic frequency scaling cannot be underestimated in terms of power consumption alone (just think of higher battery life or a million home routers working at half the power) are not actively doing this. Some lack hardware features to implement this feature, others do not simply have a driver that can use the hardware capabilities provided.
In this post, I would explain the innards of a cpu frequency scaling driver and its interface to Linux CPU frequency scaling framework.
Pre-requisites
- Obviously, first thing you might need to have a cpu that is capable of scaling the frequency up and down.
- The programmer's reference manual : Mostly CPU frequency and stuff like that would be buried deep inside the manual.
- There would be a base frequency (Mostly the CPU PLL frequency)
- There could be a multiplier and configuration registers to calculate the base frequency the cpu can run at. Similarly you can calcuate the highest frequency supported as well.
Complications do not end here, as there could be some relation between DRAM frequency and CPU frequency PLL. In that case you will have to figure out the supported combinations of CPU and DRAM frequencies. You might need help from board engineers to create the table. etc. etc. But, eventually, you need a supported frequency table and corresponding values to write in the configuration registers. With multi core cpus and SoCs, changing frequency of a cpu/core might have implications on the peripherals/other cores as well. You are warned :-)
Example
On one of the cpus I was working with, I had the following details- Clock divider value per core : each core controls four h/w threads
- PLL frequency
- Divider increment registers and Divider decrement registers
There were many constraints like some of the bits were read only, Increment reg. and decrement reg. were to be written with all 1s ..etc. You might face similar constraints as well. Assuming you have something similar to below table, we can proceed as follows.
| DIV value | Frequency(MHz) |
|---|---|
| 1 | 100 |
| 2 | 133 |
| 3 | 160 |
| 4 | 200 |
| 5 | 229 |
| 6 | 266 |
| 7 | 320 |
| 9 | 400 |
| 11 | 533 |
| 13 | 800 |
| 15 | 1200 |
| 17 | 1600 |
You may opt to support only a subset of these possible frequency ranges by dropping some DIV values. But you will not be able to support some frequency other than what your hardware actually supports.
Thus, the first step is to define an array of supported DIV values like below
static int xxx_divs[XXX_DIVS] = {1, 2, 3, 4, 5, 6, 7, 9, 11, 13, 15}; /* drop 17 */
static struct cpufreq_frequency_table xxx_freq_table[XXX_DIVS + 1];
Now, you may define your function that actually would manipulate the frequency
multiplers in the hw as below
u32 change_cpu_freq(int cpu, int dec)
{
/* body of the function that actually does the work */
return 0;
}
Next important step is to define a cpufreq_driver structure such that kernel can be made aware of the possible frequency support (i.e, what function to call to get the speed, set the speed ..etc). That is done by instantiating a cpufreq_driver structure like below
static struct cpufreq_driver xxx_cpufreq_driver = {
.owner = THIS_MODULE,
.flags = 0,
.verify = xxx_cpufreq_verify_speed,
.target = xxx_cpufreq_set_target,
.get = xxx_cpufreq_get_speed,
.init = xxx_cpufreq_driver_init,
.exit = xxx_cpufreq_driver_exit,
.name = "xxx-cpufreq",
};
There are some more fields to define if you prefer. Please take a look at
include/linux/cpufreq.h for more info. Plesae note that I have defined `target`
and not `setpolicy'
The init function for this module is,
static int __init xxx_cpufreq_init(void)
{
setup_affected_cpus(cpu_present_map);
if (build_cpufreq_table(xxx_freq_table) < 0) {
return -EFAULT;
}
return cpufreq_register_driver(&xxx_cpufreq_driver);
}
A few points to note here:
- I call a static function setup_affected_cpus() with the map of cpus present at the moment. I can do this because my h/w does not support cpu hotplugging. The objective of this function is to create a static map of cpu threads on the same core -- all of which will change frequency if any one of them changes the core frequency (all cpus in this set is driven by a single core). You may do it some different way.
- After registration, get/target/verify could be called anytime (after driver init => .init). So, I prepare the supported cpu frequency table in advance; well before calling the registration function. I guess you could do it in driver init as well
- The driver init function gets the policy structure where we can fill in details like related cpus. This function will be called for every cpu/core of the system. This is where you can setup values like transition_latency, affected cpus ..etc.
- There are a number of helper functions available @ drivers/cpufreq/freq_table.c for massaging the values you filled in cpu frequency table. You may opt to use validation functions like cpufreq_frequency_table_cpuinfo(), cpufreq_frequency_table_verify() ..etc
The workhorse function for cpu freq. driver is .target function (xxx_cpufreq_set_target() for me). This function will be passed policy, target cpu and another parameter 'relation' (which signifies whether to ceil() or floor() the value for optimal frequency). Depending on the current frequency and target frequency, you might have to call functions to incr/decr cpu frequency from this function. Most important to note is that you should call cpufreq_notify_transition(&freq, CPUFREQ_PRECHANGE) before you change the frequency and cpufreq_notify_transition(&freq, CPUFREQ_POSTCHANGE) after you have reached the target frequency.
This completes kernel driver part. You will have to enable cpu frequency governors if you wish to use them. Take a look at some examples using "userspace" governor as default
/sys/devices/system/cpu/cpu0/cpufreq # ls
affected_cpus scaling_cur_freq
cpuinfo_cur_freq scaling_driver
cpuinfo_max_freq scaling_governor
cpuinfo_min_freq scaling_max_freq
cpuinfo_transition_latency scaling_min_freq
related_cpus scaling_setspeed
scaling_available_governors
/sys/devices/system/cpu/cpu0/cpufreq # for I in *
> do
> echo $I
> cat $I
> echo "...Done"
> done
affected_cpus
0 1 2 3
...Done
cpuinfo_cur_freq
800000
...Done
cpuinfo_max_freq
800000
...Done
cpuinfo_min_freq
100000
...Done
cpuinfo_transition_latency
2000000
...Done
related_cpus
0 1 2 3
...Done
scaling_available_governors
conservative ondemand powersave userspace performance
...Done
scaling_cur_freq
800000
...Done
scaling_driver
xxx-cpufreq
...Done
scaling_governor
userspace
...Done
scaling_max_freq
800000
...Done
scaling_min_freq
100000
...Done
scaling_setspeed
800000
...Done
# Using unsupported (lower than lowest) value as frequency
/sys/devices/system/cpu/cpu0/cpufreq # echo 90000 > scaling_setspeed
/sys/devices/system/cpu/cpu0/cpufreq # cat cpuinfo_cur_freq
100000
#cpu set to correct lowest frequency
#CPU set to unsupported (intermediate) value as frequency
/sys/devices/system/cpu/cpu0/cpufreq # echo 120000 > scaling_setspeed
/sys/devices/system/cpu/cpu0/cpufreq # cat cpuinfo_cur_freq
133328
# Using unsupported (higher than highest) value as frequency
# Please note th capped highest 800M, even though h/w supports 1.2G
/sys/devices/system/cpu/cpu0/cpufreq # echo 1123000 > scaling_setspeed
/sys/devices/system/cpu/cpu0/cpufreq # cat cpuinfo_cur_freq
800000
#CPU set to supported value as frequency
/sys/devices/system/cpu/cpu0/cpufreq # echo 400000 > scaling_setspeed
/sys/devices/system/cpu/cpu0/cpufreq # cat cpuinfo_cur_freq
400000
There are finer points to consider : like if your system clock is dependent on
cpu frequency, you might need to change the clocksource to a constant frequency
timer. That will be explained in a future post. Stay tuned...
No comments:
Post a Comment