Linux PWM 驱动实验

Posted 嵌入式学习者。

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux PWM 驱动实验相关的知识,希望对你有一定的参考价值。

一、PWM 驱动简析

1、设备树下的 PWM 控制器节点

I.MX6ULL 有 8 路 PWM 输出,因此对应 8 个 PWM 控制器,所有在设备树下就有 8 个PWM 控制器节点。这 8 路 PWM 都属于 I.MX6ULL 的 AIPS-1 域,但是在设备树 imx6ull.dtsi 中分为了两部分,PWM1-PWM4 在一起,PWM5-PWM8 在一起,这 8 路 PWM 并没有全部放到一起。imx6ull.dtsi 文件中的 pwm3 节点信息如下:

pwm3: pwm@02088000 
	compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm";
	reg = <0x02088000 0x4000>;
	interrupts = <GIC_SPI 85 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_PWM3>,
				<&clks IMX6UL_CLK_PWM3>;
	clock-names = "ipg", "per";
	#pwm-cells = <2>;
;

第 2 行,compatible 属性值有两个“fsl,imx6ul-pwm”和“fsl,imx27-pwm”,所以在整个 Linux源码里面搜索这两个字符窜即可找到 I.MX6ULL 的 PWM 驱动文件。

2、PWM 子系统

Linux 内核提供了个 PWM 子系统框架,编写 PWM 驱动的时候一定要符合这个框架。PWM子系统的核心是 pwm_chip 结构体。

struct pwm_chip 
	struct device *dev;
	struct list_head list;
	const struct pwm_ops *ops;
	int base;
	unsigned int npwm;
	struct pwm_device *pwms;
	struct pwm_device * (*of_xlate)(struct pwm_chip *pc,
	const struct of_phandle_args *args);
	unsigned int of_pwm_n_cells;
	bool can_sleep;
;

第 4 行,pwm_ops 结构体就是 PWM 外设的各种操作函数集合,编写 PWM 外设驱动的时候需要开发人员实现。pwm_ops 结构体也定义在 pwm.h 头文件中,定义如下:

struct pwm_ops 
	int (*request)(struct pwm_chip *chip, //请求 PWM
	struct pwm_device *pwm);
	void (*free)(struct pwm_chip *chip, //释放 PWM
	struct pwm_device *pwm);
	int (*config)(struct pwm_chip *chip, //配置 PWM 周期和占空比
	struct pwm_device *pwm,
	int duty_ns, int period_ns);
	int (*set_polarity)(struct pwm_chip *chip, //设置 PWM 极性
	struct pwm_device *pwm,
	enum pwm_polarity polarity); 
	int (*enable)(struct pwm_chip *chip, //使能 PWM
	struct pwm_device *pwm);
	void (*disable)(struct pwm_chip *chip, //关闭 PWM
	struct pwm_device *pwm);
	struct module *owner;
;

pwm_ops 中的这些函数不一定全部实现,但是像 config、enable 和 disable 这些肯定是需要实现的,否则的话打开/关闭 PWM,设置 PWM 的占空比这些就没操作了。

PWM 子系统驱动的核心初始化 pwm_chip 结构体,然后向内核注册初始化完成以后的pwm_chip。这里就要用到 pwmchip_add 函数,此函数定义在 drivers/pwm/core.c 文件中,函数原型如下:

int pwmchip_add(struct pwm_chip *chip)

函数参数和返回值含义如下:
chip:要向内核注册的 pwm_chip。
返回值:0 成功;负数 失败。
卸载 PWM 驱动的时候需要将前面注册的 pwm_chip 从内核移除掉,这里要用到pwmchip_remove 函数,函数原型如下:

int pwmchip_remove(struct pwm_chip *chip)

函数参数和返回值含义如下:
chip:要移除的 pwm_chip。
返回值:0 成功;负数 失败。

3、PWM 驱动源码分析

我们简单分析一下 Linux 内核自带的 I.MX6ULL PWM 驱动,驱动文件前面都说了,是 pwmimx.c 这个文件。打开这个文件,可以看到,这是一个标准的平台设备驱动文件,如下所示:

static const struct of_device_id imx_pwm_dt_ids[] = 
	 .compatible = "fsl,imx1-pwm", .data = &imx_pwm_data_v1, ,
	 .compatible = "fsl,imx27-pwm", .data = &imx_pwm_data_v2, ,
	 /* sentinel */ 
;

	......

static struct platform_driver imx_pwm_driver = 
	.driver = 
		.name = "imx-pwm",
		.of_match_table = imx_pwm_dt_ids,
	,
	.probe = imx_pwm_probe,
	.remove = imx_pwm_remove,
;

module_platform_driver(imx_pwm_driver);

第 3 行,当设备树 PWM 节点的 compatible 属性值为“fsl,imx27-pwm”的话就会匹配此驱动,注意后面的.data 为 imx_pwm_data_v2,这是一个 imx_pwm_data 类型的结构体变量,内容如下:

static struct imx_pwm_data imx_pwm_data_v2 = 
	.config = imx_pwm_config_v2,
	.set_enable = imx_pwm_set_enable_v2,
;

imx_pwm_config_v2 函数就是最终操作 I.MX6ULL 的 PWM 外设寄存器,进行实际配置的函数。imx_pwm_set_enable_v2 就是具体使能 PWM 的函数。
第 14 行,当设备树节点和驱动匹配以后 imx_pwm_probe 函数就会执行。

static int imx_pwm_probe(struct platform_device *pdev)

	const struct of_device_id *of_id =
	of_match_device(imx_pwm_dt_ids, &pdev->dev);
	const struct imx_pwm_data *data;
	struct imx_chip *imx;
	struct resource *r;
	int ret = 0;

	if (!of_id)
		return -ENODEV;

	imx = devm_kzalloc(&pdev->dev, sizeof(*imx), GFP_KERNEL);	//imx 是一个 imx_chip 类型的结构体指针变量,这里为其申请内存。
	if (imx == NULL)
		return -ENOMEM;
		......
	imx->chip.ops = &imx_pwm_ops;		//
	imx->chip.dev = &pdev->dev;
	imx->chip.base = -1;
	imx->chip.npwm = 1;
	imx->chip.can_sleep = true;
	
	r = platform_get_resource(pdev, IORESOURCE_MEM, 0);	//从设备树中获取 PWM 节点中关于 PWM 控制器的地址信息
	imx->mmio_base = devm_ioremap_resource(&pdev->dev, r);
	if (IS_ERR(imx->mmio_base))
	return PTR_ERR(imx->mmio_base);
	
	data = of_id->data;
	imx->config = data->config;			//设置 imx 的 config 和 set_enable 这两个成员变量为 data->config 和data->set_enable
	imx->set_enable = data->set_enable;	//也就是 imx_pwm_config_v2 和 imx_pwm_set_enable_v2这两个函数。
	
	ret = pwmchip_add(&imx->chip);
	if (ret < 0)
	return ret;
	
	platform_set_drvdata(pdev, imx);
	return 0;

第 31 行设置 pwm_chip的 ops 操作集为 imx_pwm_ops,imx_pwm_ops 定义如下:

static struct pwm_ops imx_pwm_ops = 
	.enable = imx_pwm_enable,
	.disable = imx_pwm_disable,
	.config = imx_pwm_config,
	.owner = THIS_MODULE,
;

imx_pwm_enable、imx_pwm_disable 和 imx_pwm_config 这三个函数就是使能、关闭和配置PWM 的函数。
imx_pwm_enable、imx_pwm_disable 和 imx_pwm_config 这三个函数最终调用就是imx_pwm_config_v2 和 imx_pwm_set_enable_v2。

整 个 pwm-imx.c 文 件 里 面 , 最 终 和 I.MX6ULL 的 PWM 寄 存 器 打 交 道 的 就 是imx_pwm_config_v2 和 imx_pwm_set_enable_v2 这 两 个 函 数 , 我 们 先 来 看 一 下imx_pwm_set_enable_v2 函数,此函数用于打开或关闭对应的 PWM,函数内容如下:

static void imx_pwm_set_enable_v2(struct pwm_chip *chip, bool enable)

	struct imx_chip *imx = to_imx_chip(chip);
	u32 val;
	
	val = readl(imx->mmio_base + MX3_PWMCR);		//读取 PWMCR 寄存器的值。
	
	if (enable)									//如果 enable 为真,表示使能 PWM
		val |= MX3_PWMCR_EN;
	else										//如果 enable 不为真,表示关闭 PWM
		val &= ~MX3_PWMCR_EN;
	
	writel(val, imx->mmio_base + MX3_PWMCR);	//将新的 val 值写入到 PWMCR 寄存器中

imx_pwm_config_v2 函数用于设置 PWM 的频率和占空比,相关操作如下:

static int imx_pwm_config_v2(struct pwm_chip *chip,struct pwm_device *pwm, int duty_ns, int period_ns)

	struct imx_chip *imx = to_imx_chip(chip);
	struct device *dev = chip->dev;
	unsigned long long c;
	unsigned long period_cycles, duty_cycles, prescale;
	unsigned int period_ms;
	bool enable = test_bit(PWMF_ENABLED, &pwm->flags);
	int wait_count = 0, fifoav;
	u32 cr, sr;
	
	......
	
	c = clk_get_rate(imx->clk_per);
	c = c * period_ns;
	do_div(c, 1000000000);
	period_cycles = c;
	
	prescale = period_cycles / 0x10000 + 1;
	
	period_cycles /= prescale;
	c = (unsigned long long)period_cycles * duty_ns;
	do_div(c, period_ns);
	duty_cycles = c;
	
	/*
	* according to imx pwm RM, the real period value should be
	* PERIOD value in PWMPR plus 2.
	*/
	if (period_cycles > 2)
		period_cycles -= 2;
	else
		period_cycles = 0;
	
	writel(duty_cycles, imx->mmio_base + MX3_PWMSAR);
	writel(period_cycles, imx->mmio_base + MX3_PWMPR);
	
	cr = MX3_PWMCR_PRESCALER(prescale) |
	MX3_PWMCR_DOZEEN | MX3_PWMCR_WAITEN |
	MX3_PWMCR_DBGEN | MX3_PWMCR_CLKSRC_IPG_HIGH;
	
	if (enable)
		cr |= MX3_PWMCR_EN;
	
	writel(cr, imx->mmio_base + MX3_PWMCR);
	
	return 0;

二、PWM 驱动编写

1、修改设备树
PWM 驱动就不需要我们再编写了,NXP 已经写好了,前面我们也已经详细的分析过这个驱动源码了。我们在实际使用的时候只需要修改设备树即可,ALPHA 开发板上的 JP2 排针引出了 GPIO1_IO04 这个引脚,如下图:

1.1、添加 GPIO1_IO04 引脚信息

打开 imx6ull-alientek-emmc.dts 文件,在 iomuxc 节点下添加 GPIO1_IO04 的引脚信息,如下所示:

pinctrl_pwm3: pwm3grp 
	fsl,pins = <
		MX6UL_PAD_GPIO1_IO04__PWM3_OUT 0x110b0
	>;
;

1.2、向 pwm3 节点追加信息

前面已经讲过了,imx6ull.dtsi 文件中已经有了“pwm3”节点,但是还不能直接使用,需要在 imx6ull-alientek-emmc.dts 文件中向 pwm3 节点追加一些内容,在 imx6ull-alientek-emmc.dts 文件中加入如下所示内容:

&pwm3 
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_pwm3>;
	clocks = <&clks IMX6UL_CLK_PWM3>,
			<&clks IMX6UL_CLK_PWM3>;
	status = "okay";
;

第 3 行,pinctrl-0 属性指定 PWM3 所使用的输出引脚对应的 pinctrl 节点
第 4 和 5 行,设置时钟,第 4 行设置 ipg 时钟,第 5 行设置 per 时钟。有些 pwm 节点默认时钟源是 IMX6UL_CLK_DUMMY,这里我们需要将其改为对应的时钟,比如这里设置为IMX6UL_CLK_PWM3。PWM1-PWM8 分别对应 IMX6UL_CLK_PWM1- IMX6UL_CLK_PWM8。

1.3、屏蔽掉其他复用的 IO

检查一下设备树中有没有其他外设用到 GPIO1_IO04,如果有的话需要屏蔽掉!注意,不能只屏蔽掉 GPIO1_IO04 的 pinctrl 配置信息,也要搜索一下“gpio1 4”,看看有没有哪里用到,用到的话也要屏蔽掉。设备树修改完成以后重新编译设备树,然后使用新的设备树启动系统。

2、使能 PWM 驱动

-> Device Drivers 
	-> Pulse-Width Modulation (PWM) Support 
		-> <*> i.MX PWM support

3、PWM 驱动测试

我们可以直接在用户层来配置 PWM,进入目录/sys/class/pwm 中,如图

pwmchip0-pwmchip7 对应 I.MX6ULL 的 PWM1-PWM8,所以我们需要用到pwmchip2。

1、调出 pwmchip2 的 pwm0 子目录

echo 0 > /sys/class/pwm/pwmchip2/export

执行完成会在 pwmchip2 目录下生成一个名为“pwm0”的子目录,如图

2、使能 PWM3

echo 1 > /sys/class/pwm/pwmchip2/pwm0/enable

3、设置 PWM3 的频率
注意,这里设置的是周期值,单位为 ns,比如 20KHz 频率的周期就是 50000ns,输入如下命令:

echo 50000 > /sys/class/pwm/pwmchip2/pwm0/period

4、设置 PWM3 的占空比

这里不能直接设置占空比,而是设置的一个周期的 ON 时间,也就是高电平时间,比如20KHz 频率下 20%占空比的 ON 时间就是 10000,输入如下命令:

echo 10000 > /sys/class/pwm/pwmchip2/pwm0/duty_cycle

以上是关于Linux PWM 驱动实验的主要内容,如果未能解决你的问题,请参考以下文章

第八章

Android深度探索(卷1)HAL与驱动开发 第八章 让开发板发出声音:蜂鸣器驱动 读书笔记

[S5PV210 Linux字符驱动之PWM蜂鸣器驱动

第八章随笔

Android深度探索——第八章读书笔记及心得

Linux LED 驱动开发实验