基于宋宝华《Linux设备驱动开发详解-基于最新的Linux4.0内核》。
Linux在中断处理中引入了顶半部与底半部分分离的机制
底半部(Bottom Half)——处理不紧急的耗时操作。
底半部的机制主要有:tasklet、工作队列、软中断、线程化irq
如果中断要处理的工作很少,完全可以在顶半部全部完成。
3种类型的中断:
Linux内核周期性任务实现:
Linux内核延时:
相关中断函数解释:
request_irq() /* 申请中断 */
dev_request_irq() /* 申请中断,申请的是内核managed的资源 */
free_irq() /* 释放中断 */
disable_irq_nosync() /* 立即返回 */
disable_irq() /* 等待目前的处理完成再返回 */
一些目录:
基于宋宝华《Linux设备驱动开发详解-基于最新的Linux4.0内核》。
一切都是文件。
Linux 2.4内核的文件系统是devfs。
Linux 2.6以后的内核文件系统是udev。udev完全在用户态工作。
从图中可以看出,这里有两处不同的文件操作:Linux文件操作,C库文件操作。
Linux用户控件的文件编程有两种方法,即,通过Linxu API和通过C库函数访问文件。用户空间看不到设备驱动,能看到的只有与设备对应的文件。
Linux文件系统目录结构:
在设备驱动程序的设计中,有两个结构体需要关系:
Documents目录下的devices.txt文件描述了Linux设备号的分配情况:
Linux设计中的一个基本观点是:机制与策略分离。
Linux 2.6以后的内核引入了sysfs文件系统,sysfs被看出是与proc、devfs、devpty同类别的文件系统,该文件系统是一个虚拟的文件系统。sysfs的一个目的就是战士设备驱动模型中各组件的层次关系,其顶级目录包括block、bus、devices、class、fs、kernel、power和firmware等。
在Linux内核中,分别使用(结构体的定义位于include/linux/device.h)
设备和驱动分离,并通过总线进行匹配。
在Linux内核中,设备和驱动是分开注册的。bus_type的match()把两者联系在一起,一旦配对成功,xxx_driver的probe()就被执行。
udev的工作过程如下:
step 1:当内核检测到系统中出现了新设备后,内核会通过netlink套接字方式发送uevent。从而动态创建设备文件节点。
step 2:udev获取内核发送的信息,进行规制的匹配。
udev规则文件:
在嵌入式系统中,也可以用udev的轻量级版本mdev,mdev集成于busybox中。在编译busybox的时候,选中mdev相关项目即可。
Android也没有采用udev,它采用的是uold。uold的机制和udev是一样的。
基于宋宝华《Linux设备驱动开发详解-基于最新的Linux4.0内核》。
软件工程思想与原则:
实际的Linux驱动中,Linux内核尽量做的更多,以便于底层的驱动可以做的更少。而且,也特别强调了驱动的跨平台特性。
ARM Linux 3.x的目标:一个映像适用于多个硬件。
驱动只管驱动,设备只管设备,总线则负责匹配设备和驱动,而驱动则以标准途径拿到板级信息。
需关心总线、设备、驱动着3个实体。总线将设备和驱动绑定。在系统每注册一个设备的时候,会寻找与之匹配的驱动;相反的,在系统每注册一个驱动的时候,会寻找与之匹配的设备,而匹配有总线完成。
主机端只负责产生总线上的传输波形,而外设端只是通过标准的API来让主机端以适当的波形访问自身。涉及到4个软件模块:
基于宋宝华《Linux设备驱动开发详解-基于最新的Linux4.0内核》。
设备树(Device Tree)是一种描述硬件的数据结构。
有自己的独立语法。由一系列被命名的节点(node)和属性(Property)组成。
节点本身可包含子节点。
基本上就是画一颗电路板上CPU、总线、设备组成的树,Booloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。
设备树源文件.dts,编译后得到.dtb,BootLoader在引导Linux内核的时候会将.dtb地址告诉内核。之后,内核会展开设备树并创建和注册相关的设备。
DTS —— 文件.dts是一种ASCII文本格式的设备树描述。一个.dts文件对应一个ARM设备。可以包括.dtsi文件。.dtsi文件 —— 相当于C语言中的.h文件。SOC公用的部分或多个设备共同的部分放在这里。
一般放在arch/arm/boot/dts/目录中。
DTC —— 将.dts编译为.dtb的工具。
源代码一般放在scripts/dtc/目录中。
如果内核使能了设备树,DTC会被编译出来。对应于scripts/dtc/Makefile中的“hostprogs-y := dtc”。
DTB —— 是.dts被DTC编译后的二进制格式的设备树描述。
.dtb文件可以单独存放在一个很小的区域,也可以直接和zImage绑定在一起做出一个映像文件(编译内核时,使能CONFIG_ARM_APPENDED_DTB)。
反编译dtb文件为dts文件的命令:
$ dtc -I dtb -O dts -o test.dts ./r8a7795-salvator-xs-android.dtb
Binding —— 讲解文档,讲解设备树中的节点和属性是如何来描述设备的硬件细节的,扩展名.txt。
位于内核的Documentation/devicetree/bindings目录。
BootLoader —— Uboot从v1.1.2开始支持设备树。使能方法:在编译Uboot时,在config文件中,加入 “#define CONFiG_OF_LIBFDT”。
常用的 OF API (实现代码位于内核的drivers/of目录下):
of_machine_is_compatible();
of_device_is_compatible();
of_find_compatible_node(); /* 寻找节点 */
of_find_device_by_node(); /* 获取与节点对应的platform_device */
of_property_read_bool(); /* 读取属性,bool值 */
of_property_read_u8_array(); /* 读取属性, 数组 */
of_property_read_u8(); /* 读取属性,一个值 */
of_property_read_string(); /* 读取字符串 */
of_address_to_resource(); /* 内存映射 */
irq_of_parse_and_map(); /* 解析中断 */
另外,还可以参考官方文档:
基于宋宝华《Linux设备驱动开发详解-基于最新的Linux4.0内核》。
电源管理的唯一目标是 省电 。
CMOS电路中的功耗与电压的平方成正比,与频率成正比:P∝fV^2
图中,有两处频率变换的地方:
CPUFreq的核心层位于drivers/cpufrq/cpufreq.c:
每个SOC的具体CPUFreq驱动实例只需要实现电压、频率表,以及从硬件层面完成这些变化。
SOC CPUFreq驱动只是设定了CPU的频率参数,以及提供了设置频率的途径。
一个SOC的CPUFreq驱动实例(drivers/cpufreq/s3c64xx-cpufreq.c):
...
static struct s3c64xx_dvfs s3c64xx_dvfs_table[] = {
[0] = { 1000000, 1150000 },
[1] = { 1050000, 1150000 },
[2] = { 1100000, 1150000 },
[3] = { 1200000, 1350000 },
[4] = { 1300000, 1350000 },
};
static struct cpufreq_frequency_table s3c64xx_freq_table[] = {
{ 0, 0, 66000 },
{ 0, 0, 100000 },
{ 0, 0, 133000 },
{ 0, 1, 200000 },
{ 0, 1, 222000 },
{ 0, 1, 266000 },
{ 0, 2, 333000 },
{ 0, 2, 400000 },
{ 0, 2, 532000 },
{ 0, 2, 533000 },
{ 0, 3, 667000 },
{ 0, 4, 800000 },
{ 0, 0, CPUFREQ_TABLE_END },
};
static int s3c64xx_cpufreq_set_target(struct cpufreq_policy *policy,
unsigned int index)
{
...
ret = regulator_set_voltage(vddarm,
dvfs->vddarm_min,
dvfs->vddarm_max);
...
ret = clk_set_rate(policy->clk, new_freq * 1000);
...
}
#ifdef CONFIG_REGULATOR
static void s3c64xx_cpufreq_config_regulator(void)
{
...
cpufreq_for_each_valid_entry(freq, s3c64xx_freq_table) {
dvfs = &s3c64xx_dvfs_table[freq->driver_data];
found = 0;
for (i = 0; i < count; i++) {
v = regulator_list_voltage(vddarm, i);
if (v >= dvfs->vddarm_min && v <= dvfs->vddarm_max)
found = 1;
}
...
}
#endif
static int s3c64xx_cpufreq_driver_init(struct cpufreq_policy *policy)
{
...
policy->clk = clk_get(NULL, "armclk");
...
s3c64xx_cpufreq_config_regulator();
...
regulator_put(vddarm);
clk_put(policy->clk);
...
}
static struct cpufreq_driver s3c64xx_cpufreq_driver = {
.flags = CPUFREQ_NEED_INITIAL_FREQ_CHECK,
.verify = cpufreq_generic_frequency_table_verify,
.target_index = s3c64xx_cpufreq_set_target,
.get = cpufreq_generic_get,
.init = s3c64xx_cpufreq_driver_init,
.name = "s3c",
};
static int __init s3c64xx_cpufreq_init(void)
{
return cpufreq_register_driver(&s3c64xx_cpufreq_driver);
}
module_init(s3c64xx_cpufreq_init);
究竟频率依据哪种标准,进行何种变化,完全有CPUFreq的策略(policy)决定:
用户空间可通过/sys/devices/system/cpu/cpux/cpufreq节点来设置CPUFreq(采用userspace策略),则运行如下命令:
\# echo userspace > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
\# echo 700000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed
CPUFreq子系统主要会发出两种通知(notifier):
CPUFREQ_NOTIFY:所有注册的notifier都会被告知新的策略已经被设置
CPUFREQ_POSTCHANGE:已经完成变更
除了CPU以外,一些非CPU设备也支持多个操作频率和电压,存在多个OPP。Linux 3.2之后的内核也支持针对这种非CPU设备的DVFS,该套子系统为DEVFreq,位于drivers/devfreq目录。
某个domain所支持的<频率,电压>对的集合被称为Operating Performance Point,缩写为OPP。
一个OPP实例:
static struct omap_opp_def __initdata omap44xx_opp_def_list[]={
/* MPU OPP1 - OPP50 */
OPP_INITIALIZER("mpu", true, 300000000, OMAP4430_VDD_MPU_OPP50_UV),
...
}
...
int __init omap4_opp_init(void)
{
...
r = omap_init_opp_table(omap44xx_opp_def_list,
ARRAY_SIZE(omap44xx_opp_def_list));
...
}
device_initcall(omap4_opp_init);
int __init omap_init_opp_table(struct omap_opp_def *opp_def, u32 opp_def_size)
{
...
for(i = 0; i< opp_def_size;i++,opp_def++){
...
r = opp_add(dev, opp_def->freq, opp_def->u_volt);
...
}
return 0;
}
下面两个API分别用于获取与某OPP对应的电压和频率:
unsigned long opp_get_voltage(struct opp *opp);
unsigned long opp_get_freq(struct opp *opp);
当某个CPUFreq驱动想将CPU设置为某一频率的时候,它可能会同时设置电压,其代码流程为:
soc_switch_to_freq_voltage(freq)
{
/* do thing */
rcu_read_lock();
opp = opp_find_freq_ceil(dev, &freq);
v = opp_get_voltage(opp);
rcu_read_unlock();
if(v)
regulator_set_voltage(..,v);
/* do other things */
}
big.LITTLE架构的设计旨在为适当的作业分配恰当的处理器。
:
基于宋宝华《Linux设备驱动开发详解-基于最新的Linux4.0内核》。
Linux设备驱动会以内核模块的形式出现,因此,学会编写Linux内核模块编程是学习Linux设备驱动的先决条件。
为了避免编译后的内核尺寸过大,有些功能不会被编译进内核。
当需要使用这些功能的时候,相应的代码被动态的加载到内核。这种机制被称为模块(Module):
一个Linux模块主要由以下几个部分组成:
/*
* 模块说明
*
*/
static int __init xxx_init(void)
{
}
module_init(xxx_init);
static int __exit xxx_exit(void)
{
}
module_exit(xxx_exit);
MODULE_AUTHOR("XXX");
MODULE_LICENSE("XXX");
...
编译后,生成xxx.ko目标文件。
命令 | 描述 |
---|---|
insmod ./xxx.ko | 加载模块 |
modprob ./xxx.ko | 加载模块及该模块依赖的其他模块 |
modprob -r ./xxx.ko | 卸载模块及该模块依赖的其他模块 |
lsmod | 获得系统中已经加载的所有模块及模块间的依赖关系(读取/proc/modules文件);已经加载的模块信息位于/sys/module目录 |
modinfo <模块名>模块名> | 获得模块的信息 |