驱动篇:底层驱动移植(四)(摘录)
发布日期:2021-06-29 11:35:19 浏览次数:2 分类:技术文章

本文共 10768 字,大约阅读时间需要 35 分钟。

驱动篇:底层驱动移植(四)(摘录)

时钟驱动

在一个 SoC 中,晶振、 PLL 、驱动和门等会形成一个时钟树形结构,在 Linux 2.6 中,也存有clk_get_rate ()、clk_set_rate ()、 clk_get_parent ()、 clk_set_parent ()等通用 API ,但是这些 API 由每个 SoC 单独实现,而且各个 SoC 供应商在实现方面的差异很大,于是内核增加了一个新的通用时钟框架以解决这个碎片化问题。之所以称为通用时钟,是因为这个通用主要体现在:
1 )统一的 clk 结构体,统一的定义于 clk.h 中的 clk API ,这些 API 会调用统一的 clk_ops 中的回调函数;这个统一的clk 结构体的定义如代码清单所示:
clk 结构体

struct clk {
const char *name; const struct clk_ops *ops; struct clk_hw *hw; char **parent_names; struct clk **parents; struct clk *parent; struct hlist_head children; struct hlist_node child_node;...};clk_ops 定义是关于时钟使能、禁止、计算频率等的操作集

clk_ops 结构体

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); 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); void (*init)(struct clk_hw *hw); int (*debug_init)(struct clk_hw *hw, struct dentry *dentry);};

2 )对具体的 SoC 如何去实现针对自己 SoC 的 clk 驱动,如何提供硬件特定的回调函数的方法也进行了统一。在代码清单中这个通用的 clk 结构体中,第 4 行的 clk_hw 是联系 clk_ops 中回调函数和具体硬件细节的纽带, clk_hw 中只包含通用时钟结构体的指针以及具体硬件的 init 数据,如代码清单所示:

clk_hw 结构体

struct clk_hw {
struct clk_core *core; struct clk *clk; const struct clk_init_data *init;};

其中的 clk_init_data 包含了具体时钟的名称、可能的父级时钟的名称列表 parent_names 、可能的父级时钟数量num_parents 等,实际上这些名称的匹配对建立时钟间的父子关系功不可没,如代码清单所示:

clk_init_data 结构体

struct clk_init_data {
const char *name; const struct clk_ops *ops; const char * const *parent_names; u8 num_parents; unsigned long flags;};

从 clk 核心层到具体芯片 clk 驱动的调用顺序为:

clk_enable ( clk );  -----> clk->ops->enable ( clk->hw );

通用的 clk API (如 clk_enable )在调用底层 clk 结构体的 clk_ops 成员函数(如 clk->ops->enable )时,会将 clk->hw 传递过去。一般在具体的驱动中会定义针对特定 clk (如 foo )的结构体,该结构体中包含 clk_hw 成员以及硬件私有数据:

struct clk_foo {
struct clk_hw hw;... hardware specific data goes here ...};

并定义 to_clk_foo ()宏,以便通过 clk_hw 获取 clk_foo :

#define to_clk_foo(_hw) container_of(_hw, struct clk_foo, hw)

在针对 clk_foo 的 clk_ops 的回调函数中,我们便可以通过 clk_hw 和 to_clk_foo 最终获得硬件私有数据,并访问硬件读写寄存器以改变时钟的状态:

struct clk_ops clk_foo_ops {
.enable = &clk_foo_enable;.disable = &clk_foo_disable;};int clk_foo_enable(struct clk_hw *hw){
struct clk_foo *foo;foo = to_clk_foo(hw);/*访问硬件读写寄存器以改变时钟的状态*/...return 0;};

在具体的 clk 驱动中,需要通过 clk_register ()以及它的变体注册硬件上所有的 clk ,通过 clk_register_clkdev ()注册 clk 的一个 lookup (这样可以通过 con_id 或者 dev_id 字符串寻找到这个 clk ),这两个函数的原型为:

struct clk *clk_register(struct device *dev, struct clk_hw *hw);int clk_register_clkdev(struct clk *clk, const char *con_id,const char *dev_fmt, ...);

另外,针对不同的 clk 类型(如固定频率的 clk 、 clk 门、 clk 驱动等), clk 子系统又提供了几个快捷函数以完成clk_register ()的过程:

struct clk *clk_register_fixed_rate(struct device *dev, const char *name,const char *parent_name, unsigned long flags,unsigned long fixed_rate);struct clk *clk_register_gate(struct device *dev, const char *name,const char *parent_name, unsigned long flags,void __iomem *reg, u8 bit_idx,u8 clk_gate_flags, spinlock_t *lock);struct clk *clk_register_divider(struct device *dev, const char *name,const char *parent_name, unsigned long flags,void __iomem *reg, u8 shift, u8 width,u8 clk_divider_flags, spinlock_t *lock);

以 drivers/clk/clk-prima2.c 为例,与该驱动对应的芯片 SiRFprimaII 的外围接了一个 26MHz 的晶振和一个 32.768kHz 的RTC 晶振,在 26MHz 晶振的后面又有 3 个 PLL ,当然 PLL 后面又接了更多的 clk 节点,则它的相关驱动代码如清单:

clk 驱动案例

static unsigned long pll_clk_recalc_rate(struct clk_hw *hw,unsigned long parent_rate){
unsigned long fin = parent_rate;struct clk_pll *clk = to_pllclk(hw); ...}static long pll_clk_round_rate(struct clk_hw *hw, unsigned long rate,unsigned long *parent_rate){
...}static int pll_clk_set_rate(struct clk_hw *hw, unsigned long rate,unsigned long parent_rate){
...}static struct clk_ops std_pll_ops = {
.recalc_rate = pll_clk_recalc_rate,.round_rate = pll_clk_round_rate,.set_rate = pll_clk_set_rate,25};static const char *pll_clk_parents[] = {
"osc",};static struct clk_init_data clk_pll1_init = {
.name = "pll1",.ops = &std_pll_ops,.parent_names = pll_clk_parents,.num_parents = ARRAY_SIZE(pll_clk_parents),};static struct clk_init_data clk_pll2_init = {
.name = "pll2",.ops = &std_pll_ops,.parent_names = pll_clk_parents,.num_parents = ARRAY_SIZE(pll_clk_parents),};static struct clk_init_data clk_pll3_init = {
.name = "pll3",.ops = &std_pll_ops,.parent_names = pll_clk_parents,.num_parents = ARRAY_SIZE(pll_clk_parents),};static struct clk_pll clk_pll1 = {
.regofs = SIRFSOC_CLKC_PLL1_CFG0,.hw = {
.init = &clk_pll1_init,},};static struct clk_pll clk_pll2 = {
.regofs = SIRFSOC_CLKC_PLL2_CFG0,.hw = {
.init = &clk_pll2_init,},};static struct clk_pll clk_pll3 = {
.regofs = SIRFSOC_CLKC_PLL3_CFG0,.hw = {
.init = &clk_pll3_init,},};void __init sirfsoc_of_clk_init(void){
.../* These are always available (RTC and 26MHz OSC)*/clk = clk_register_fixed_rate(NULL, "rtc", NULL,CLK_IS_ROOT, 32768);BUG_ON(!clk);clk = clk_register_fixed_rate(NULL, "osc", NULL,CLK_IS_ROOT, 26000000);BUG_ON(!clk);clk = clk_register(NULL, &clk_pll1.hw);BUG_ON(!clk);clk = clk_register(NULL, &clk_pll2.hw);BUG_ON(!clk);clk = clk_register(NULL, &clk_pll3.hw);BUG_ON(!clk); ...}

另外,目前内核更加倡导的方法是通过设备树来描述电路板上的时钟树,以及时钟和设备之间的绑定关系。通常我们需要在 clk 控制器的节点中定义 #clock-cells 属性,并且在 clk 驱动中通过of_clk_add_provider ()注册时钟控制器为一个时钟树的提供者( Provider ),并建立系统中各个时钟和索引的映射表,如:

Clock   ID---------------------------rtc  0osc  1pll1  2pll2  3pll3  4mem  5sys   6 security  7dsp  8gps  9mf  10

在每个具体的设备中,对应的 .dts 节点上的 clocks=<&clks index> 属性指向其引用的 clk 控制器节点以及使用的时钟的索引,如:

gps@a8010000 {
compatible = "sirf,prima2-gps";reg = <0xa8010000 0x10000>;interrupts = <7>;clocks = <&clks 9>;};

要特别强调的是,在具体的设备驱动中,一定要通过通用 clk API 来操作所有的时钟,而不要直接通过读写 clk 控制器的寄存器来进行,这些 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);int clk_prepare(struct clk *clk);void clk_unprepare(struct clk *clk);void clk_disable(struct clk *clk);static inline int clk_prepare_enable(struct clk *clk);static inline void clk_disable_unprepare(struct clk *clk);unsigned long clk_get_rate(struct clk *clk);int clk_set_rate(struct clk *clk, unsigned long rate);struct clk *clk_get_parent(struct clk *clk);int clk_set_parent(struct clk *clk, struct clk *parent);

值得一提的是,名称中含有 prepare 、 unprepare 字符串的 API 是内核后来才加入的,过去只有 clk_enable ()和clk_disable ()。只有 clk_enable ()和 clk_disable ()带来的问题是,有时候,某些硬件使能 / 禁止时钟可能会引起睡眠以使得使能 / 禁止不能在原子上下文进行。加上 prepare 后,把过去的 clk_enable ()分解成不可在原子上下文调用的 clk_prepare ()(该函数可能睡眠)和可以在原子上下文调用的 clk_enable ()。而clk_prepare_enable ()则同时完成准备和使能的工作,当然也只能在可能睡眠的上下文调用该 API。

dmaengine 驱动

dmaengine 是一套通用的 DMA 驱动框架,该框架为具体使用 DMA 通道的设备驱动提供了一套统一的 API ,而且也定义了用具体的 DMA 控制器实现这一套 API 的方法。对于使用 DMA 引擎的设备驱动而言,发起 DMA 传输的过程变得整洁了,如在 sound 子系统的 sound/soc/soc-dmaengine-pcm.c 中,会使用 dmaengine 进行周期性的 DMA 传输,相关的代码如清单所示:
dmaengine API 的使用

static int dmaengine_pcm_prepare_and_submit(struct snd_pcm_substream *substream){
struct dmaengine_pcm_runtime_data *prtd = substream_to_prtd(substream); struct dma_chan *chan = prtd->dma_chan; struct dma_async_tx_descriptor *desc; enum dma_transfer_direction direction; unsigned long flags = DMA_CTRL_ACK; ... desc = dmaengine_prep_dma_cyclic(chan, substream->runtime->dma_addr, snd_pcm_lib_buffer_bytes(substream), snd_pcm_lib_period_bytes(substream), direction, flags); ... desc->callback = dmaengine_pcm_dma_complete; desc->callback_param = substream; prtd->cookie = dmaengine_submit(desc);}int snd_dmaengine_pcm_trigger(struct snd_pcm_substream *substream, int cmd){
struct dmaengine_pcm_runtime_data *prtd = substream_to_prtd(substream); int ret; switch (cmd) {
case SNDRV_PCM_TRIGGER_START: ret = dmaengine_pcm_prepare_and_submit(substream); ... dma_async_issue_pending(prtd->dma_chan); break; case SNDRV_PCM_TRIGGER_RESUME: case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: dmaengine_resume(prtd->dma_chan); break;} ...}}

这个过程可分为 4 步:

1 )通过 dmaengine_prep_dma_xxx ()初始化一个具体的 DMA 传输描述符(本例中为结构体dma_async_tx_descriptor 的实例 desc ,本例是一个周期性 DMA ,因此第 10 行调用的是dmaengine_prep_dma_cyclic ())。
2 )通过 dmaengine_submit ()将该描述符插入 dmaengine 驱动的传输队列(见第 17 行)。
3 )在需要传输的时候通过类似 dma_async_issue_pending ()的调用启动对应 DMA 通道上的传输(见第 28 行)。
4 ) DMA 的完成,或者周期性 DMA 完成了一个周期,都会引发 DMA 传输描述符的完成回调函数被调用(本例中的赋值在第 15 行,对应的回调函数是 dmaengine_pcm_dma_complete )。

也就是不管具体硬件的 DMA 控制器是如何实现的,在软件意义上都抽象为了设置 DMA 描述符、将 DMA 描述符插入传输队列以及启动 DMA 传输的过程。

除了前文提到的用 dmaengine_prep_dma_cyclic ()定义周期性 DMA 传输外,还有一组类似的 API 可以用来定义各种类型的 DMA 描述符,特定硬件的 DMA 驱动的主要工作就是实现封装在内核 dma_device 结构体中的这些成员函数(定义在 include/linux/dmaengine.h 头文件中):

/*** struct dma_device - info on the entity supplying DMA services* @device_prep_dma_memcpy: prepares a memcpy operation* @device_prep_dma_xor: prepares a xor operation* @device_prep_dma_xor_val: prepares a xor validation operation* @device_prep_dma_pq: prepares a pq operation* @device_prep_dma_pq_val: prepares a pqzero_sum operation* @device_prep_dma_memset: prepares a memset operation* @device_prep_dma_interrupt: prepares an end of chain interrupt operation* @device_prep_slave_sg: prepares a slave dma operation* @device_prep_dma_cyclic: prepare a cyclic dma operation suitable for audio.* The function takes a buffer of size buf_len. The callback function will* be called after period_len bytes have been transferred.* @device_prep_interleaved_dma: Transfer expression in a generic way.*/

在底层的 dmaengine 驱动实例中,一般会组织好这个 dma_device 结构体,并通过 dma_async_device_register ()完成注册。在其各个成员函数中,一般会通过链表来管理 DMA 描述符的运行、 free 等队列。dma_device 的成员函数 device_issue_pending ()用于实现 DMA 传输开启的功能,每当 DMA 传输完成后,在驱动中注册的中断服务程序的顶半部或者底半部会调用 DMA 描述符 dma_async_tx_descriptor 中设置的回调函数,该回调函数来源于使用 DMA 通道的设备驱动。典型的 dmaengine 驱动可见于 drivers/dma/ 目录下的 sirf-dma.c 、 omap-dma.c 、 pl330.c 、 ste_dma40.c 等。

总结

移植 Linux 到全新的 SMP SoC 上,需在底层提供定时器节拍、中断控制器、 SMP 启动、 GPIO 、时钟、 pinctrl 等功能,这些底层的功能被封装好后,其他设备驱动只能调用内核提供的通用 API 。这良好地体现了内核的分层设计,即驱动都调用与硬件无关的通用 API ,而这些 API 的底层实现则更多的是填充内核规整好的回调函数。

Linux 内核社区针对 pinctrl 、时钟、 GPIO 、 DMA 提供独立的子系统,既给具体的设备驱动提供了统一的 API ,进一步提高了设备驱动的跨平台性,又为每个 SoC 和设备实现这些底层 API 定义好了条条框框,从而可以在最大程度上避免每个硬件实现过多的冗余代码。

转载地址:https://blog.csdn.net/zytgg123456/article/details/110683901 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:C++篇:基础入门
下一篇:驱动篇:底层驱动移植(三)(摘录)

发表评论

最新留言

逛到本站,mark一下
[***.202.152.39]2024年04月27日 07时26分28秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章