一、clk框架简介
linux内核中实现了一个CLK子系统,用于对上层提供各模块(例如需要时钟信号的外设,USB等)的时钟驱动接口,对下层提供具体SOC的时钟操作细节:
一般情况下,在可运行linux的处理器平台中,都存在非常复杂的时钟树(clock tree)关系,也一定会有一个非常庞大和复杂的树状图,用于描述与时钟相关的器件,以及这些器件输出的clock关系。查看手册都会存在类似下列的内容:
一款处理器中与时钟相关的器件主要包括有:
用于产生 CLOCK 的 Oscillator(有源振荡器,也称作谐振荡器)或者Crystal(无源振荡器,也称晶振)。
用于倍频的 PLL(锁相环,Phase Locked Loop)。
用于分频的Divider。
用于多路选择的 MUX。
用于CLOCK ENABLE控制的与门。
使用 CLOCK 的硬件模块(也可称为CONSUMER)。
linux内核中与clk框架相关源文件如下:
/include/linux/clk.h /include/linux/clkdev.h /include/linux/clk-provider.h /include/linux/clk-conf.h ------------------------------------------------------ /drivers/clk/clk-devres.c /drivers/clk-bulk.c /drivers/clkdev.c /drivers/clk.c /drivers/clk-divider.c /drivers/clk-fixed-factor.c /drivers/clk-fixed-rate.c /drivers/clk-gate.c /drivers/clk-multiplier.c /drivers/clk-mux.c /drivers/clk-composite.c /drivers/clk-fractional-divider.c /drivers/clk-gpio.c /drivers/clk-conf.c
二、clk框架接口
1、基于linux时钟子系统对接底层时钟操作的API
linux时钟子系统对接底层,也就是具体硬件常用的API可视为clk provider常用的接口函数,定义在linux/include/linux/clk-provider.h文件中。不同版本linux内核中对于clk-probider.h实现的而接口存在出入,参见源码更进一步获取接口和使用方法。
注册/注销时钟
//注册一个新的时钟。通常在设备驱动程序中使用,以向时钟框架添加新的时钟源。 struct clk *clk_register(struct device *dev, struct clk_hw *hw); //带资源管理注册时钟 struct clk *devm_clk_register(struct device *dev, struct clk_hw *hw); //卸载时钟 void clk_unregister(struct clk *clk); //带资源管理卸载时钟 void devm_clk_unregister(struct device *dev, struct clk *clk);
2、驱动中常使用的API
芯片厂家会根据clk框架,对下层(即底层硬件)设计出接口,以供上层驱动接口调用,在内核中,提供的接口主要由/include/linux/clk.h文件导出,使用这些API接口时,需包含linux/clk.h头文件:
#include
获取struct clk指针:
struct clk *devm_clk_get(struct device *dev, const char *id)(推荐使用,可以自动释放) struct clk *clk_get(struct device *dev, const char *id) static inline struct clk *devm_clk_get_optional(struct device *dev, constchar *id) //(推荐使用,整组获取,整组开关) static inline int __must_check devm_clk_bulk_get(struct device *dev, intnum_clks, struct clk_bulk_data *clks) static inline int __must_check devm_clk_bulk_get_optional(struct device *dev,int num_clks, struct clk_bulk_data *clks) static inline int __must_check devm_clk_bulk_get_all(struct device *dev,struct clk_bulk_data **clks)
获取/设置时钟频率
//根据给定的目标频率计算最接近的可实现频率。这个函数通常在设置时钟频率之前调用,以确保设置的频率是硬件支持的频率之一。 long clk_round_rate(struct clk *clk, unsigned long rate) //获取时钟频率 unsigned long clk_get_rate(struct clk *clk) //设置时钟频率 int clk_set_rate(struct clk *clk, unsigned long rate)
准备/使能clk:
/* 开时钟前调用,可能会造成休眠,所以把休眠部分放到这里,可以原子操作的放到enable里 */ int clk_prepare(struct clk *clk) /* 停止clock后的善后工作,可能会睡眠。*/ void clk_unprepare(struct clk *clk) /* 原子操作,打开时钟,这个函数必须在产生实际可用的时钟信号后才能返回,不会睡眠 */ int clk_enable(struct clk *clk) /* 原子操作,关闭时钟,不会睡眠 */ void clk_disable(struct clk *clk)
上述两套API的本质,是把CLOCK的启动/停止分为Atomic和Non-atomic两个阶段,以方便实现和调用。因此上面所说的“不会睡眠/可能会睡眠”,有两个角度的含义:
一是告诉底层的CLOCK Driver,需把可能引起睡眠的操作,放到Prepare()/Unprepare()中实现,一定不能放到Enable()/Disable()中;
二是提醒上层使用CLOCK的Driver,调用Prepare/Unprepare 接口时可能会睡眠,千万不能在Atomic上下文(例如内部包含Mutex 锁、中断关闭、Spinlock 锁保护的区域)调用,而调用Enable()/Disable()接口则可放心。
另外,CLOCK的Enable()/Disable()为什么需要睡眠呢?例如Enable PLL CLOCK,在启动PLL后,需要等待它稳定,然而PLL的稳定时间是很长的,因此这段时间要需要把CPU让出(进程睡眠),不然就会浪费CPU了。
最后,为什么会实现合在一起的clk_prepare_enable()/clk_disable_unprepare()接口呢?如果调用者能确保是在Non-atomic上下文中调用,就可以顺序调用prepare()/enable()、disable()/unprepared(),为了方便Colck框架就封装了这两个接口。
备注:使用clk_prepare_enable / clk_disable_unprepare,clk_prepare_enable / clk_disable_unprepare(或者clk_enable / clk_disable) 必须成对,以使引用计数正确。
三、CLK核心的数据结构和API
1、struct clk_notifier
stuct clk_notifier用于将CLK与通知器进行关联,也就是定义clk的通知器,基于srcu实现。该结构实现如下(/linux/include/linux/clk.h):
struct clk_notifier { struct clk *clk; //与该通知器关联的clk。 struct srcu_notifier_head notifier_head; //用于这个CLK的blocking_notifier_head通知器。 struct list_head node; };
常用API:
//注册一个通知块(notifier block),以便在指定时钟发生事件(例如频率变化)时接收通知。 int clk_notifier_register(struct clk *clk, struct notifier_block *nb); //注销一个通知块 int clk_notifier_unregister(struct clk *clk, struct notifier_block *nb); //带资源管理注册一个通知块(notifier block),以便在指定时钟发生事件(如频率变化)时接收通知。确保在设备驱动程序卸载时自动注销通知块。 int devm_clk_notifier_register(struct device *dev, struct clk *clk,struct notifier_block *nb);
2、struct clk_core
struct clk_core为clk框架的私有结构,定义如下:
struct clk_core { const char *name; //clk核心名称。 const struct clk_ops *ops; //该clk核心对应的ops。 struct clk_hw *hw; struct module *owner; struct device *dev; struct device_node *of_node; struct clk_core *parent; struct clk_parent_map *parents; u8 num_parents; u8 new_parent_index; unsigned long rate; unsigned long req_rate; unsigned long new_rate; struct clk_core *new_parent; struct clk_core *new_child; unsigned long flags; bool orphan; bool rpm_enabled; unsigned int enable_count; unsigned int prepare_count; unsigned int protect_count; unsigned long min_rate; unsigned long max_rate; unsigned long accuracy; int phase; struct clk_duty duty; struct hlist_head children; struct hlist_node child_node; struct hlist_head clks; unsigned int notifier_count; #ifdef CONFIG_DEBUG_FS struct dentry *dentry; struct hlist_node debug_node; #endif struct kref ref; };
从上述结构的组成元素可知,struct clk_core是struct device的子类,因为一款SOC的时钟关系一般以“树状”进行组织,在struct clk_core中提供描述父clk_core和子clk_core的组成元素。
3、struct clk_ops
struct clk_ops描述硬件时钟的回调操作,并由驱动程序通过clk_*API调用。该结构定义如下:
struct clk_ops { int (*prepare)(struct clk_hw *hw); void (*unprepare)(struct clk_hw *hw); int (*is_prepared)(struct clk_hw *hw); void (*unprepare_unused)(struct clk_hw *hw); int (*enable)(struct clk_hw *hw); void (*disable)(struct clk_hw *hw); int (*is_enabled)(struct clk_hw *hw); void (*disable_unused)(struct clk_hw *hw); int (*save_context)(struct clk_hw *hw); void (*restore_context)(struct clk_hw *hw); unsigned long (*recalc_rate)(struct clk_hw *hw, unsigned long parent_rate); long (*round_rate)(struct clk_hw *hw, unsigned long rate, unsigned long *parent_rate); int (*determine_rate)(struct clk_hw *hw, struct clk_rate_request *req); int (*set_parent)(struct clk_hw *hw, u8 index); u8 (*get_parent)(struct clk_hw *hw); int (*set_rate)(struct clk_hw *hw, unsigned long rate, unsigned long parent_rate); int (*set_rate_and_parent)(struct clk_hw *hw, unsigned long rate, unsigned long parent_rate, u8 index); unsigned long (*recalc_accuracy)(struct clk_hw *hw, unsigned long parent_accuracy); int (*get_phase)(struct clk_hw *hw); int (*set_phase)(struct clk_hw *hw, int degrees); int (*get_duty_cycle)(struct clk_hw *hw, struct clk_duty *duty); int (*set_duty_cycle)(struct clk_hw *hw, struct clk_duty *duty); int (*init)(struct clk_hw *hw); void (*terminate)(struct clk_hw *hw); void (*debug_init)(struct clk_hw *hw, struct dentry *dentry); };
prepare:准备启动时钟。该回调直到时钟完全准备好才会返回,调用clk_enable是安全的。这个回调的目的是允许时钟实现执行任何可能休眠的初始化。在prepare_lock被持有的情况下调用。
unprepare:将时钟从准备状态中释放出来。该函数通常会撤销在.prepare回调中完成的工作。在prepare_lock持有的情况下调用。
is_prepared:查询硬件以确定时钟是否准备好。允许此函数休眠,如果此操作不是设置后,将使用prepare计数。(可选的)
unprepare_unused:自动取消时钟准备。只从clk_disable_unused调用,用于特殊需要的时钟准备。在持有prepare互斥锁的情况下调用。这个函数可能会休眠。
enable:自动启用时钟。该函数直到时钟正在生成一个有效的时钟信号之前不能返回,供消费者设备驱动使用。在enable_lock持有情况下调用,该函数必须不能睡眠。
disable:自动禁用时钟。在enable_lock持有情况下调用,该函数必须不能睡眠。
is_enabled:查询硬件以确定时钟是否开启。这个函数不能休眠。如果此操作不是设置,则enable计数将被使用,该函数可选。
disable_unused:自动禁用时钟。只从clk_disable_unused调用用于特殊需要的gate时钟。在enable_lock持有的情况下调用,这个函数不能睡眠。
save_context:保存时钟上下文,为断电做准备。
restore_context:在电源恢复后恢复时钟上下文。
recalc_rate:通过查询硬件重新计算该时钟的速率。如果驱动程序不能计算出这个时钟的速率,它必须返回0。如果此callback未设置,则时钟速率将初始化为0(可选的)。
round_rate:给定一个目标速率作为输入,实际上返回由时钟支持最接近的速率,父速率是一个input/output参数。
determine_rate:给定目标速率作为输入,返回实际上是由时钟支撑的最接近的速率。
set_parent:改变这个时钟的输入源,设置父时钟。
get_parent:查询硬件以确定时钟的父时钟。
set_rate:改变这个时钟的速率。
set_rate_and_parent:更改此时钟的速率和父时钟。
recalc_accuracy:重新计算一下这个钟的精度。时钟的准确性以PPB(十亿分之一)表示。父精度为输入参数。
get_phase:查询硬件以获取时钟的当前相位。
set_phase:将时钟信号的相位按指定的度数移位。
get_duty_cycle:查询硬件,获取时钟当前占空比。
set_duty_cycle:将占空比应用于由分子(第二个参数)和分母(第三个参数)指定的时钟信号。
init:执行特定于平台的初始化魔术。
terminate:释放由init分配的资源。
debug_init:为这个时钟设置特定类型的debugfs条目,在为这个时钟创建了debugfs目录条目之后,调用它一次。上述结构中的回调函数的实现需根据具体情况而定,每个callback的具体含义根据名称可以知道,CLK时钟框架对上层开放的API都会间接调用到这些callback,下表是一个clock硬件矩阵表,用于描述特定应用场景下需要实现的callback:
参数说明:
y表示强制,必须实现。
n表示包含该回调无效或没有必要实现。
空单元格表示是可选的,或者必须根据具体情况评估实现。
4、struct clk_gate
struct clk_gate用于描述门控时钟,该结构定义如下:
struct clk_gate { struct clk_hw hw; //处理公共接口和特定于硬件的接口。 void __iomem *reg; //寄存器控制门。 u8 bit_idx; //单比特控制门。 u8 flags; //特定硬件的falg标志。 spinlock_t *lock; //自旋锁。 };
常用API:
to_clk_gate() clk_register_gate()/clk_unregister_gate()
5、struct clk
struct clk用于描述一个clk设备,该结构定义如下:
struct clk { struct clk_core *core; //表示clk核心。 struct device *dev; //clk设备的父设备。 const char *dev_id; //设备id。 const char *con_id; unsigned long min_rate; //最小频率。 unsigned long max_rate; //最大频率。 unsigned int exclusive_count; //独占计数。 struct hlist_node clks_node; //clk链表。 };
6、struct clk_hw
struct clk_hw用于描述特定硬件实列的结构,该结构定义如下:
struct clk_hw { struct clk_core *core; //clk核心。 struct clk *clk; //clk设备。 const struct clk_init_data *init; //描述clk初始化数据 };
struct clk_hw中包含了struct clk_core和struct clk。可以看成是clk框架中对clk核心和clk设备的封装。
7、struct clk_divider
struct clk_divider描述可调的分频时钟,该结构定义如下:
struct clk_divider { struct clk_hw hw; //处理公共接口和特定硬件的接口 void __iomem *reg; //分频器的寄存器 u8 shift; //分频位域的偏移量 u8 width; //分频位域的宽度 u8 flags; //标志 const struct clk_div_table *table; //数组的值/除数对,最后一项div = 0。 spinlock_t *lock; //注册锁 };
具有影响其输出频率的可调分压器的时钟。实现.recalc_rate,.set_rate和.round_rate。
常用API:
clk_register_divider()/clk_unregister_divider() clk_hw_register_divider()/clk_hw_unregister_divider()
8、struct clk_mux
struct clk_mux用于描述多路复用器的时钟,该结构定义如下:
struct clk_mux { struct clk_hw hw; void __iomem *reg; const u32 *table; u32 mask; u8 shift; u8 flags; spinlock_t *lock; };
上述结构中组成元素几乎与struct clk_divider一样。
常用API:
void clk_unregister_mux(struct clk *clk); void clk_hw_unregister_mux(struct clk_hw *hw);
9、struct clk_fixed_factor
struct clk_fixed_factor用于倍频和分频时钟。该结构定义如下:
struct clk_fixed_factor { struct clk_hw hw; //处理公共接口和特定硬件的接口。 unsigned int mult; //倍频器 unsigned int div; //分频器 };
具有固定乘法器和除法器的时钟。输出频率为父时钟速率除以div再乘以mult。在.recalc_rate,.set_rate和.round_rate中实现。
10、struct clk_fractional_divider
struct clk_fractional_divider用于描述可调分数的分频时钟,该结构定义如下:
struct clk_fractional_divider { struct clk_hw hw; //处理公共接口和特定硬件的接口 void __iomem *reg; //用于分频器的寄存器 u8 mshift; //分频位域分子的偏移量 u8 mwidth; //分频位域分子的宽度 u8 nshift; //分频位域分母的偏移量 u8 nwidth; //分频位域分母的宽度 u8 flags; //标志位 void (*approximation)(struct clk_hw *hw, //近似方法的callback unsigned long rate, unsigned long *parent_rate, unsigned long *m, unsigned long *n); spinlock_t *lock; //注册锁 };
11、struct clk_multiplier
struct clk_multiplier结构用于描述可调的倍频时钟,该结构定义如下:
struct clk_multiplier { struct clk_hw hw; //处理公共接口和特定硬件的接口 void __iomem *reg; //倍频器的寄存器 u8 shift; //乘法位域的偏移量 u8 width; //乘法位域的宽度 u8 flags; //标志 spinlock_t *lock; //注册锁 };
12、struct clk_composite
struct clk_composite结构用于描述多路复用器、分频器和门控时钟的组合时钟。该结构定义如下:
struct clk_composite { struct clk_hw hw; //处理公共接口和特定硬件的接口 struct clk_ops ops; //clk对应的ops的callback struct clk_hw *mux_hw; //处理复合和硬件特定多路复用时钟 struct clk_hw *rate_hw; //处理复合和硬件特定的频率时钟 struct clk_hw *gate_hw; //处理之间的组合和硬件特定的门控时钟 const struct clk_ops *mux_ops; //对mux的时钟ops const struct clk_ops *rate_ops; //对rate的时钟ops const struct clk_ops *gate_ops; //对gate的时钟ops };
常用API:
to_clk_composite() clk_register_composite()/clk_unregister_composite()
clk核心数据结构如下图所示:
四、CLK调试
参见debugfs文件系统下的文件可推知目前系统中存在的clk情况,使用如下命令:
cat /sys/debug/kernel/clk/clk_summary
查看目前系统的时钟树(clk_tree)。例如:
可以在用户空间通过/sys设置时钟节点:
//get rate: cat /sys/kernel/debug/aclk_gmac0/clk_rate //set rate: echo 24000000 > /sys/kernel/debug/aclk_gmac0/clk_rate //打开clk: echo 1 > /sys/kernel/debug/aclk_gmac0/clk_enable_count //关闭clk: echo 0 > /sys/kernel/debug/aclk_gmac0/clk_enable_count
五、CLK信息导出
1、与debugfs调试信息相关的初始化
当内核支持debugfs且开启对clk的调试支持,我们可以在/sys文件系统路径中的clk目录下查看关于系统中所有注册的clk信息,例如:
每个目录代表一个clk信息,其目录下包含如下信息:
从内核源码角度,创建debugfs调试目录或文件由clk_debug_init()完成:
static int __init clk_debug_init(void) { struct clk_core *core; #ifdef CLOCK_ALLOW_WRITE_DEBUGFS pr_warn(" "); pr_warn("******************************************************************** "); pr_warn("** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE ** "); pr_warn("** ** "); pr_warn("** WRITEABLE clk DebugFS SUPPORT HAS BEEN ENABLED IN THIS KERNEL ** "); pr_warn("** ** "); pr_warn("** This means that this kernel is built to expose clk operations ** "); pr_warn("** such as parent or rate setting, enabling, disabling, etc. ** "); pr_warn("** to userspace, which may compromise security on your system. ** "); pr_warn("** ** "); pr_warn("** If you see this message and you are not debugging the ** "); pr_warn("** kernel, report this immediately to your vendor! ** "); pr_warn("** ** "); pr_warn("** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE ** "); pr_warn("******************************************************************** "); #endif rootdir = debugfs_create_dir("clk", NULL); debugfs_create_file("clk_summary", 0444, rootdir, &all_lists, &clk_summary_fops); debugfs_create_file("clk_dump", 0444, rootdir, &all_lists, &clk_dump_fops); debugfs_create_file("clk_orphan_summary", 0444, rootdir, &orphan_list, &clk_summary_fops); debugfs_create_file("clk_orphan_dump", 0444, rootdir, &orphan_list, &clk_dump_fops); mutex_lock(&clk_debug_lock); hlist_for_each_entry(core, &clk_debug_list, debug_node) clk_debug_create_one(core, rootdir); inited = 1; mutex_unlock(&clk_debug_lock); return 0; }
clk_debug_init()函数由late_initcall()(/drivers/clk.c)导出。
六、clk驱动设计
1、底层驱动(clk-provider)
对于一款SOC,特定厂家都会针对时钟编写对应的驱动。包括用于以下功能的文件:
用于倍频的 PLL(锁相环,Phase Locked Loop)。
用于分频的Divider。
用于多路选择的 MUX。
用于CLOCK ENABLE控制的与门。
使用 CLOCK 的硬件模块(也可称为CONSUMER)。
在设计这些clk驱动时,本质上是实现对应struct clk_ops下的callback后,调用clk_register注册进linux内核的时钟框架。不同的时钟器件在内核中都存在与之对应的数据结构,且开放有对应的API接口,将其注册到内核中。
例如Nxp的Imx6ul这款SOC,在/arch/arm/mach-imx/clk-imx6ull.c中则实现了对应时钟框架的底层驱动,由imx6ul_clocks_init()实现:
CLK_OF_DECLARE(imx6ul, "fsl,imx6ul-ccm", imx6ul_clocks_init);
存在下图类似的SOC时钟描述语句:
上述语句中无论是imx_clk_mux()还是imx_clk_pllv3()都会调用clk_register()向内核注册时钟资源。
2、驱动层clk
当底层(clk-provider)设计完成后,在驱动层(也称为消费者(Consumer))则可以很方便的获取对应的clk句柄,并可以进行enable/disable时钟等操作了。
常用API有:
//查找并获取对时钟产生器的引用 struct clk *clk_get(struct device *dev, const char *id); struct clk *devm_clk_get(struct device *dev, const char *id); //当时钟源处于运行状态时,通知系统 int clk_enable(struct clk *clk); //当时钟源不再使用时,通知系统 void clk_disable(struct clk *clk); clk_prepare_enable()
在内核源码中,可以发现很多的驱动设计都会使用到时钟子系统,用于对外设的控制和开启/停止。
例如,在Nxp提供的一个名为imx.c(/drivers/tty/serial)的通用uart驱动中,在.probe中则会首先进行时钟相关的操作:
static int serial_imx_probe(struct platform_device *pdev) { struct imx_port *sport; void __iomem *base; int ret = 0; struct resource *res; int txirq, rxirq, rtsirq; sport = devm_kzalloc(&pdev->dev, sizeof(*sport), GFP_KERNEL); if (!sport) return -ENOMEM; ret = serial_imx_probe_dt(sport, pdev); if (ret > 0) serial_imx_probe_pdata(sport, pdev); else if (ret < 0) return ret; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(base)) return PTR_ERR(base); rxirq = platform_get_irq(pdev, 0); txirq = platform_get_irq(pdev, 1); rtsirq = platform_get_irq(pdev, 2); sport->port.dev = &pdev->dev; sport->port.mapbase = res->start; sport->port.membase = base; sport->port.type = PORT_IMX, sport->port.iotype = UPIO_MEM; sport->port.irq = rxirq; sport->port.fifosize = 32; sport->port.ops = &imx_pops; sport->port.rs485_config = imx_rs485_config; sport->port.rs485.flags = SER_RS485_RTS_ON_SEND | SER_RS485_RX_DURING_TX; sport->port.flags = UPF_BOOT_AUTOCONF; init_timer(&sport->timer); sport->timer.function = imx_timeout; sport->timer.data = (unsigned long)sport; sport->clk_ipg = devm_clk_get(&pdev->dev, "ipg"); if (IS_ERR(sport->clk_ipg)) { ret = PTR_ERR(sport->clk_ipg); dev_err(&pdev->dev, "failed to get ipg clk: %d ", ret); return ret; } sport->clk_per = devm_clk_get(&pdev->dev, "per"); if (IS_ERR(sport->clk_per)) { ret = PTR_ERR(sport->clk_per); dev_err(&pdev->dev, "failed to get per clk: %d ", ret); return ret; } sport->port.uartclk = clk_get_rate(sport->clk_per); if (sport->port.uartclk > IMX_MODULE_MAX_CLK_RATE) { ret = clk_set_rate(sport->clk_per, IMX_MODULE_MAX_CLK_RATE); if (ret < 0) { dev_err(&pdev->dev, "clk_set_rate() failed "); return ret; } } sport->port.uartclk = clk_get_rate(sport->clk_per); /* * Allocate the IRQ(s) i.MX1 has three interrupts whereas later * chips only have one interrupt. */ if (txirq > 0) { ret = devm_request_irq(&pdev->dev, rxirq, imx_rxint, 0, dev_name(&pdev->dev), sport); if (ret) return ret; ret = devm_request_irq(&pdev->dev, txirq, imx_txint, 0, dev_name(&pdev->dev), sport); if (ret) return ret; } else { ret = devm_request_irq(&pdev->dev, rxirq, imx_int, 0, dev_name(&pdev->dev), sport); if (ret) return ret; } imx_ports[sport->port.line] = sport; platform_set_drvdata(pdev, sport); return uart_add_one_port(&imx_reg, &sport->port); }
在上述代码中,与时钟相关的操作有四个地方:
获取了时钟,在这个通用uart驱动中的其他相关的回调中,则会依托于时钟实现这些回调函数,例如imx_startup():
综上所述,可见时钟框架在linux设备驱动的实现中非常重要,只要与外设相关的驱动实现几乎都需要使用到时钟框架。
全部0条评论
快来发表一下你的评论吧 !