基于宋宝华《Linux设备驱动开发详解-基于最新的Linux4.0内核》。
为了让Linux在一个全新的SoC上运行,需要提供大量的底层支撑:
当前Linux多采用无节拍(根据系统的运行情况,以事件驱动的方式动态决定下一个节拍在何时产生)方案,并支持高精度定时器,内核的配置一般会使能NO_HZ和HIGH_RES_TIMERS。
实现:clock_event_device、clocksource。
...
static struct irqaction xxx_timer_irq = {
.name = "xxx_tick",
.flags = IRQF_TIMER,
.irq = 0,
.handler = xxx_timer_interrupt,
.dev_id = &xxx_clockevent,
}
对于多核处理器来说,一般的做法是给每个核分配一个独立的定时器,各个核根据自身的运行情况动态的设置自己时钟中断发生的时刻。
处理器间通讯,通过IPI(Internal Processor Interrupt)广播到其他核。在R-Car SoC,是通过MFIS(Multifunctional Interface)实现。
芯片供应商需要提供以下API的底层支持:
request_irq()
enable_irq() /* 与具体中断控制器有关 */
disable_irq() /* 与具体中断控制器有关 */
local_irq_enable() /* 与具体中断控制器无关 */
local_irq_disable() /* 与具体中断控制器无关 */
指令:
CPSID/CPSIE—— >= Arm v6
MRS/MSR—— < Arm v6
在内核中,通过irq_chip结构体来描述中断控制器。
struct irq_chip{
...
void (*irq_ack)(struct irq_data *data); /* 清中断 */
void (*irq_mask)(struct irq_data *data); /* mask */
...
void (*irq_unmask)(struct irq_data *data); /* unmask */
...
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type); /* 设置触发方式 */
}
对于有多个中断控制器的,其之间可能是级联的。
drivers/pinctrl/sirf/pinctrl-sirf.c中,显示了如何实现级联:
static int sirfsoc_gpio_probe(struct device_node *np)
{
...
gpiochip_set_chained_irqchip(&sgpio->chip.gc,
&sirfsoc_irq_chip,
bank->parent_irq, /* 上一级中断号 */
sirfsoc_gpio_handle_irq); /* 上一级中断服务程序 */
...
}
...
static void sirfsoc_gpio_handle_irq(unsigned int irq, struct irq_desc *desc)
{
...
if((status & 0x01) && (ctrl & SIRFSOC_GPIO_CTL_INTR_EN_MASK))
{
generic_handle_irq(irq_find_mapping(gc->irqdomain,
idx+bank->id * SIRFSOC_GPIO_BANK_SIZE));
}
...
}
以上,假设,中断服务程序为deva_isr()。
如果GPIO0_5中断发生的时候,内核的调用顺序是:
sirfsoc_gpio_handle_irq()
->
generic_handle_irq()
->
deva_isr().
每个CPU都有一个自身的ID。
一般在上电时,ID不是0的CPU将自身置于WFI或WFE状态,并等待CPU0给其发CPU核间中断或事件(一般通过SEV指令)以唤醒它。
#echo 0 > /sys/devices/system/cpu/cpu1/online #卸载CPU1,并将CPU1上的任务全部迁移到其他CPU中
#echo 1 > /sys/devices/system/cpu/cpu1/online #再次启动CPU1。之后,CPU1会主动参与系统中各个CPU之间要运行任务的负载均衡工作
CPU0唤醒其他CPU的动作在内核中被封装为一个smp_operations的结构体。
struct smp_operations{
...
void (*smp_init_cpus)(void);
void (*smp_prepare_cpus)(void);
void (*smp_secondary_init)(void);
void (*smp_boot_secondary)(void);
...
}
在dirvers/gpio下实现了通用的基于gpiolib的GPIO驱动,其中定义了一个通用的用于描述底层GPIO控制器的gpio_chip结构体。
struct gpio_chip{
...
int (*request)(..);
int (*free)(..);
int (*direction_input)(..);
int (*direction_output)(..);
void (*set)(...);
...
}
clk结构体:
struct clk_init_data{
const char *name;
const struct clk_ops *ops;
const char **parent_name;
u8 num_parenets;
unsigned long flags;
}
struct clk_ops{
int (*prepare)(...);
...
int (*enable)(...);
int (*disable)(...);
...
int (*set_rate)(...);
...
}
struct clk_hw{
struc clk *clk;
const struct clk_init_data *init;
}
struct clk{
const char *name;
const struc clk_ops *ops;
struct clk_hw *hw;
...
}
配置一个或一组引脚的功能和特性
dmaengine是一套通用的DMA驱动框架,为具体使用DMA通道的设备驱动提供一套通用的API,而且也定义了用具体的DMA 控制器实现这一套API的方法。
使用DMA引擎的过程可分为几步:
...
desc = dmaengine_prep_dma_cyclic(...); /* 初始化描述符 */
...
prtd->cookie = dmaengine_submit(desc); /* 将该描述符插入dmaengine驱动的传输队列 */
...
dma_async_issue_pending(prtd_dma_chan); /* DMA传输开始 */
基于宋宝华《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架构的设计旨在为适当的作业分配恰当的处理器。
: