第十六节 PWM 子系统–pwm 波形输出实验

PWM 子系统用于管理PWM 波的输出,与我们之前学习的其他子系统类似,PWM 具体实现代码由芯片厂商提供并默认编译进内核,而我们可以使用内核(pwm 子系统)提供的一些接口函数来实现具体的功能,例如使用PWM 波控制显示屏的背光、控制无源蜂鸣器等等。

pwm 子系统功能单一,很少单独使用。PWM 子系统的使用也很简单,我们这章通过一个极简的PWM 子系统驱动来简单认识一下PWM 子系统。其中讲解的一些接口函数后面的复杂驱动可能会用到。

PWM 子系统简介

在MP157 中pwm 子系统使用的是TIM 外设。有关TIM 外设的介绍可以参考MP157 参考手册的40~45 章节,全为各种定时器的介绍,这里不再介绍。使用了PWM 子系统后和具体硬件相关的内容芯片厂商已经写好了,我们唯一要做的就是在设备树(或者是设备树插件)中声明使用的引脚。

PWM 子系统相关资料,可在内核源码中查看参考文档:

Documentation/devicetree/bindings/pwm/pwm.txt

Documentation/devicetree/bindings/pwm/pwm-stm32.txt

PWM 设备结构体

在驱动中使用pwm_device 结构体代表一个PWM 设备。结构体原型如下所示:

列表1: pwm_device 结构体

struct pwm_device {
	const char 			*label;
	unsigned long 		flags;
	unsigned int		hwpwm;
	unsigned int 		pwm;
	struct pwm_chip 	*chip;
	void 				*chip_data;

	unsigned int 		period;
	unsigned int 		duty_cycle;
	enum pwm_polarity 	polarity;
};

pwm_device 结构体中几个重要的参数介绍如下,

  • period:设置PWM 的周期,这里的单位是纳秒(ns)。例如我们要输出一个1MHz 的PWM波,那么period 需要设置为1000。
  • duty_cycle :设置占空比,如果是正常的输出极性,这个参数指定PWM 波一个周期内高电平持续时间,单位还是ns,很明显duty_cycle 不能大于period。如果设置非输出反相,则该参数用于指定一个周期内低电平持续时间。
  • polarity :参数用于指定输出极性,即PWM 输出是否反相。它是一个枚举类型,如下所示。

列表2: pwm_polarity 枚举类型

enum pwm_polarity {
	PWM_POLARITY_NORMAL,
	PWM_POLARITY_INVERSED,
};
  • PWM_POLARITY_NORMAL :表示正常模式,不反相。
  • PWM_POLARITY_INVERSED :表示输出反相。

pwm 的申请和释放函数

PWM 使用之前要申请,不用时及时释放。申请和释放函数很多,共分为四组,介绍如下:

列表3: 第一组pwm 的申请和释放函数

struct pwm_device *pwm_request(int pwm, const char *label);
void pwm_free(struct pwm_device *pwm);

这是旧的系统使用的pwm 申请和释放函数,现在已经弃用,看到之后认识即可。这里不做介绍。

列表4: 第二组pwm 的申请和释放函数

struct pwm_device *pwm_get(struct device *dev, const char *con_id)
void pwm_put(struct pwm_device *pwm)
  • pwm_get :PWM 申请函数
  • pwm_put :PWM 释放函数
  • 参数dev : 从哪个设备获取PWM, 内核会在dev 设备的设备树节点中根据参数“con_id”查找,判断依据是con_id 与设备树节点的”pwm-names”相同。
  • 参数con_id : 如果设备中只用了一个PWM 则可以将* * 参数con_id** 设置为NULL,并且在设备树节点中不用设置“pwm-names”属性。
  • 返回值:获取成功后返回得到的pwm。失败返回NULL。
  • 在不使用pwm 设备时使用pwm_put 释放pwm。参数为pwm_get 得到的pwm_device 结构体类型指针。

列表5: 第三组pwm 的申请和释放函数

struct pwm_device *devm_pwm_get(struct device *dev, const char *con_id)
void devm_pwm_put(struct device *dev, struct pwm_device *pwm)

这一组函数是对上一组函数的封装,使用方法和第二组相同,优点是当驱动移除时自动注销申请的pwm。

列表6: 第四组pwm 的申请和释放函数

/*---------------第四组---------------*/
struct pwm_device *of_pwm_get(struct device_node *np, const char *con_id)
struct pwm_device *devm_of_pwm_get(struct device *dev, struct device_node *np,
									const char *con_id)
  • of_pwm_get 函数:从指定的设备树节点获取PWM。
  • 参数np 指定从哪个设备节点获取PWM。
  • 参数con_id 作用和前几组函数一样。
  • 返回值是获取得到的PWM,失败则返回NULL。
    函数devm_of_pwm_get 是对of_pwm_get 函数的封装,区别是它有三个参数,参数dev 指定那个设备要获取PWM ,其他两个与of_pwm_get 函数相同,它的优点是在驱动移除之前自动注销申请的pwm。

pwm 配置函数和使能/停用函数

申请成功后只需使用函数配置pwm 的频率和占空比然后使能输出即可在设定的引脚上输出PWM 波。函数很简单,如下所示。

列表7: pwm 配置函数和启动/停用函数

int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns)
int pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity)
int pwm_enable(struct pwm_device *pwm)
void pwm_disable(struct pwm_device *pwm)

函数pwm_config 用于配置PWM 的频率和占空比,需要注意的是这里是通过设置PWM 一个周期的时间和高电平时间来设置PWM 的频率和占空比,单位都是ns。函数int pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity) 用于设置PWM 极性,需要注意的是如果这里设置PWM 为负极性则函数pwm_config 中的参数duty_ns 设置的是一个周期内低电平时间。使用pwm_enable 和pwm_disable 函数使能和停用pwm。

pwm 输出实验

由于PWM 子系统很少单独使用,这里仅仅用一个极简的示例驱动程序介绍PWM 子系统的使用。我们把开发板引出的UART3_TX 引脚复用为定时器2 的PWM 输出,在驱动程序中通过设置输出的占空比,并使用示波器观察、验证输出是否正确。

示例程序主要包含两部分内容,第一,添加相应的设备树节点(这里使用设备树插件)。第二,编写测试驱动程序。

添加pwm 相关设备树插件

首先简单介绍一下设备树中的PWM 相关内容。打开“stm32mp157c.dtsi”文件,直接搜索“pwm”,可在文件中找到如下内容。

列表8: pwm 节点

timers2:timer@40000000 {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "st,stm32-timers";
	reg = <0x40000000 0x400>;
	clocks = <&rcc TIM2_K>;
	clock-names = "int";
	dmas = <&dmamux1 18 0x400 0x5>,
		<&dmamux1 19 0x400 0x5>,
		<&dmamux1 20 0x400 0x5>,
		<&dmamux1 21 0x400 0x5>,
		<&dmamux1 22 0x400 0x5>;
	dma-names = "ch1", "ch2", "ch3", "ch4", "up";
	status = "disabled";

	pwm {
		compatible = "st,stm32-pwm";
		#pwm-cells = <3>;
		status = "disabled";
	};

	timer@1 {
		compatible = "st,stm32h7-timer-trigger";
		reg = <1>;
		status = "disabled";
	};
};

这里就是PWM 驱动对应的设备树节点,这是pwm 子系统的控制节点,可以看到它设置了MP157芯片定时器外设的时钟、DMA、寄存器地址等等。这样的节点有非常多,对应着MP157 片上定时器外设的数量。简单了解即可,我们不会去修改它。

使用pwm 时,只需要引用该设备树节点,并添加一些属性信息,如下所示:

列表9: pwm 属性信息

pwms = <&PWMn id period_ns>;
pwm-names = "name";
  • pwms :属性是必须的,它共有三个属性值

&PWMn :指定使用哪个pwm,stm32mp157c.dtsi 文件中的tim 节点都有定义,在引用时可以起别名如pwm2

id :pwm 的id 通常设置为0。

period_ns :用于设置周期。单位是ns。

  • pwm-names :定义pwm 设备名字。

本实验只使用了一个gpio 设备树插件源码如下所示。

列表10: 设备树插件

// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause)
/*
* Copyright (C) STMicroelectronics 2018 - All Rights Reserved
* Author: Alexandre Torgue <alexandre.torgue@st.com>.
*/

/dts-v1/;
/plugin/;
#include <dt-bindings/pinctrl/stm32-pinfunc.h>
#include <dt-bindings/input/input.h>
#include <dt-bindings/mfd/st,stpmic1.h>
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

/ {

	fragment@0 {
		target = <&timers2>;
		__overlay__ {
			status = "okay";
			/delete-property/dmas;
			/delete-property/dma-names;
			pwm2: pwm {
				/* configure PWM pins on TIM2_CH3 */
				pinctrl-0 = <&pwm2_pins_a>;
				pinctrl-1 = <&pwm2_sleep_pins_a>;
				pinctrl-names = "default", "sleep";
				/* enable PWM on TIM2 */
				#pwm-cells = <2>;
				status = "okay";
			};
		};
	};

	fragment@1 {
		target = <&pinctrl>;
		__overlay__ {
			/* select TIM2_CH3 alternate function 1 on 'PB10' */
			pwm2_pins_a: pwm2-0 {
				pins {
					pinmux = <STM32_PINMUX('B', 10, AF1)>;
					bias-pull-down;
					drive-push-pull;
					slew-rate = <0>;
				};
			};
			/* configure 'PB10' as analog input in low-power mode */
			pwm2_sleep_pins_a: pwm2-sleep-0 {
				pins {
					pinmux = <STM32_PINMUX('B', 10, ANALOG)>;
				};
			};
		};
	};

	fragment@2 {
		target-path="/";
		__overlay__{
			pwm_test {
				compatible = "pwm_test";
				status = "okay";
				front {
					pwm-names = "test_tim2_ch3_pwm2";
					pwms = <&pwm2 2 1000000>;
				};
			};
		};
	};
};
  • 第18-31 行,此节内容插入到timers2 节点中,由于本节要将使用的引脚UART3_TX 可被复用tim2 的pwm 输出功能,那么就要开启timers2 功能,status = “okay”。在timers2 节点下包含一个“pwm”子节点,在此子节点中我们添加了UART3_TX 引脚的复用属性,复用为&pwm2_pins_a 节点定义的属性。
  • 第38-51 行,此节内容插入到pinctrl 节点中,这里定义了UART3_TX 引脚的两种状态,一是pwm2_pins_a:将UART3_TX 引脚也就是PB10 的属性复用为TIM2_CH3(查阅MP157的产品手册可得)。二是pwm2_sleep_pins_a:将UART3_TX 引脚设置为模拟模式,在低功耗模式会使用。
  • 第59-65 行,此节内容插入到根节点中,定义了一个pwm_test 节点,compatible 为”pwm_test”,用于匹配我们自己写的驱动出现。此节点包含一个“front”子节点,子节点内定义了pwm 属性信息,这里我们使用PWM2 的通道2(TIM2_CH3),频率设置为100KHz(周期为50000ns,计算得到频率为100KHz)

此设备树插件做了三个功能,将TIM2 定时器开启,设置UART3_TX(PB10)的引脚复用,定义一个节点供我们的驱动程序使用PWM 功能。

注意,此设备树插件部分内容与LCD 的设备树插件冲突,实验前注意取消LCD 的设备树插件加载。

驱动程序实现

驱动程序具体代码如下:

列表11: 注册平台设备

static const struct of_device_id of_pwm_leds_match[] = {
	{.compatible = "pwm_test"},
	{},
};

static struct platform_driver led_pwm_driver = {
	.probe = led_pwm_probe_new,
	.remove = led_pwm_remove,
	.driver = {
		.name = "test_tim2_ch3_pwm2",
		.of_match_table = of_pwm_leds_match,
	},
};

/*
* 驱动初始化函数
*/
static int __init pwm_leds_platform_driver_init(void)
{
	int DriverState;
	DriverState = platform_driver_register(&led_pwm_driver);
	return 0;
}

/*
* 驱动注销函数
*/
static void __exit pwm_leds_platform_driver_exit(void)
{
	printk(KERN_ERR " pwm_leds_exit\n");
	/* 注销平台设备*/
	platform_driver_unregister(&led_pwm_driver);
}

module_init(pwm_leds_platform_driver_init);
module_exit(pwm_leds_platform_driver_exit);

MODULE_LICENSE("GPL");
  • 第1-4 行,设置设备树节点的匹配信息。
  • 第6-13 行,填充platform_driver 结构体。
  • 第18-23 行,采用了注册平台设备的方式来注册我们的驱动程序。
  • 第28-33 行,注销平台设备驱动。

平台设备与设备节点匹配成功后我们就可以很容易从设备树中获取信息,而不必使用of 函数直接从设备树节点中获取,当然获取设备树节点的方法有很多种。

我们在.prob 函数中申请、设置、使能PWM,具体代码如下:

列表12: prob 函数

static int led_pwm_probe(struct platform_device *pdev)
{
	int ret = 0;
	struct device_node *child; // 保存子节点
	struct device *dev = &pdev->dev;
	printk("match success \n");

	/*--------------第一部分-----------------*/
	child = of_get_next_child(dev->of_node, NULL);
	if (child)
	{
		/*--------------第二部分-----------------*/
		pwm_test = devm_of_pwm_get(dev, child, NULL);
		if (IS_ERR(pwm_test))
		{
			printk(KERN_ERR" pwm_test,get pwm error!!\n");
			return -1;
		}
	}
	else
	{
		printk(KERN_ERR" pwm_test of_get_next_child error!!\n");
		return -1;
	}



	/*--------------第三部分-----------------*/
	pwm_config(pwm_test, 1000, 5000);
	pwm_set_polarity(pwm_test, PWM_POLARITY_INVERSED);
	pwm_enable(pwm_test);

	return ret;
}

static int led_pwm_remove(struct platform_device *pdev)
{
	pwm_config(pwm_test, 0, 5000);
	pwm_free(pwm_test);
	return 0;
}
  • 第9 行,获取子节点,在设备树插件中,我们把PWM 相关信息保存在pwm_test 的子节点中,所以这里首先获取子节点。
  • 第13 行,在子节点获取成功后我们使用devm_of_pwm_get 函数获取pwm,由于节点内只有一个PWM 这里将最后一个参数直接设置为NULL,这样它将获取第一个PWM。
  • 第29-31 行,依次调用pwm_config、pwm_set_polarity、pwm_enable 函数,配置PWM,设置输出极性、使能PWM 输出,需要注意的是这里设置的极性为负极性,这样pwm_config 函数第二个参数设置的就是pwm 波的一个周期内低电平计数次数,数值越大低电平持续时间越长。

实验准备

在板卡上的部分GPIO 可能会被系统占用,在使用前请根据需要修改/boot/uEnv.txt 文件,可注释掉某些设备树插件的加载,重启系统,释放相应的GPIO 引脚。

如本节实验中,可能在鲁班猫系统中默认使能了LCD 的设备功能。引脚被占用后,设备树可能无法再加载或驱动中无法再申请对应的资源。

方法参考如下:

在这里插入图片描述

取消LCD 设备树插件,以释放系统对应LCD 资源,并添加PWM 子系统实验的设备树插件,操作如下:

在这里插入图片描述

dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-pwm-sub-system.dtbo

如若运行代码时出现“Device or resource busy”或者运行代码卡死等等现象,请按上述情况检查并按上述步骤操作。

如出现Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root 用户权限,简单的解决方案是在执行语句前加入sudo 或以root 用户运行程序。

通过内核工具编译设备树插件

设备树插件与设备树一样都是使用DTC 工具编译,只不过设备树编译为.dtb。而设备树插件需要编译为.dtbo。我们可以使用DTC 编译命令编译生成.dtbo,但是这样比较繁琐、容易出错。

我们可以修改内核目录/arch/arm/boot/dts/overlays 下的Makefile 文件,添加我们编辑好的设备树插件。并把设备树插件文件放在和Makefile 文件同级目录下。以进行设备树插件的编译。

在这里插入图片描述

在内核的根目录下执行如下命令即可:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- stm32mp157_ebf_defconfig

make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs

生成的.dtbo 位于内核根目录下的“/arch/arm/boot/dts/overlays”目录下。

在这里插入图片描述

本章的PWM 子系统的设备树插件为“stm-fire-pwm-sub-system-overlay.dts” ,编译之后就会在/arch/arm/boot/dts/overlays 目录下生成同名的stm-fire-pwm-sub-system.dtbo 文件。得到.dtbo 后,下一步就是将其加载到系统中。

添加设备树插件文件

将上小节中编译出的设备树插件stm-fire-pwm-sub-system.dtbo 添加到开发板目录/usr/lib/linux-image-4.19.94-stm-r1/overlays/ 中重启开发板即可。

编译驱动程序及测试程序

本节实验使用的Makefile 如下所示:

列表13: Makefile(位于…/linux_driver/button_interrupt/interrupt)

KERNEL_DIR=../ebf_linux_kernel/build_image/build

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH CROSS_COMPILE

obj-m := pwm_sub_system.o

all:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

.PHONY:clean
clean:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

将配套的驱动代码如:pwm_sub_system 放置在与内核同级目录下,并在驱动目录中输入如下命令来编译驱动模块及测试程序:

make

下载验证

将编译好的驱动、应用程序、设备树插件并拷贝到开发板,这里就不再赘述这一部分内容了,前面的章节中都有详细介绍。

在加载模块之前,先查看/boot/uEnv.txt 文件是否加载了板子上原有的与LCD 相关设备树插件。如果之前开启了LCD 相关的设备树插件,记得先屏蔽掉。记得添加PWM 子系统的设备树插件后重启开发板。

重启后,直接使用insmod 命令加载驱动。

使用如下命令,可以查看系统当前的PWM 状态:

cat /sys/kernel/debug/pwm

在这里插入图片描述

使用示波器可以看到设定的PWM 波(如果不更改例程配置,pwm 频率为100KHz,占空比80%)。

在这里插入图片描述


参考资料:嵌入式Linux 驱动开发实战指南-基于STM32MP1 系列

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值