I.MX6ULL ARM驱动开发--- pinctrl 和 gpio 子系统的蜂鸣器驱动

引言

  Linux 是一个庞大而完善的系统, 尤其是驱动框架,像 GPIO 这种最基本的驱动不可能采用“原始”的裸机驱动开发方式,否则就相当于你买了一辆车,结果每天推着车去上班。Linux 内核提供了 pinctrl 和 gpio 子系统用于 GPIO 驱动,可以借助 pinctrl 和 gpio 子系统来简化 GPIO 驱动开发。

一、pinctrl 和 gpio 子系统

1、pinctrl 子系统

  大多数 SOC 的 pin 都是支持复用的,比如 I.MX6ULL 的 GPIO1_IO03 既可以作为普通的 GPIO 使用,也可以作为 I2C1 的 SDA 等等。此外我们还需要配置 pin 的电气特性,比如上/下 拉、速度、驱动能力等等。传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置 方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。pinctrl 子系统就是为了解决这个问题而引 入的,pinctrl 子系统主要工作内容如下:

  ① 获取设备树中 pin 信息。
  ② 根据获取到的 pin 信息来设置 pin 的复用功能
  ③ 根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。

  对于我们使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成,pinctrl 子系统源码目录为 drivers/pinctrl。

(1)设备树中添加 pinctrl 节点模板

① 创建对应的节点

  同一个外设的 PIN 都放到一个节点里面,打开 imx6ull-alientek-emmc.dts,在 iomuxc 节点中的“imx6ul-evk”子节点下添加“pinctrl_test”节点,注意!节点前缀一定要为“pinctrl_”。需要添加一个属性,属性名字一定要为“fsl,pins”,因为对于 I.MX 系列 SOC 而言,pinctrl 驱动程序是通过读取“fsl,pins”属性值来获取 PIN 的配置信息,最后在“fsl,pins”属性中添加具体的 PIN 配置信息,完成以后如下所示:

pinctrl_test: testgrp { 
	fsl,pins = < 
		MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 config /*config 是具体设置值*/
	>;
};

② PIN配置详解

iomuxc: iomuxc@020e0000 { 
	compatible = "fsl,imx6ul-iomuxc"; 
	reg = <0x020e0000 0x4000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_hog_1>;
		imx6ul-evk { 
		pinctrl_hog_1: hoggrp-1 { 
			fsl,pins = < 
			MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059
			MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT 0x17059
			MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x17059
			MX6UL_PAD_GPIO1_IO00__ANATOP_OTG1_ID 0x13058
			>;
......
		};
	};
};

fsl,pins = < MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059 >;

  以 UART1_RTS_B 这个 PIN 为例,讲解一下如何添加 PIN 的配置信息,UART1_RTS_B 这个 PIN 是作为 SD 卡的检测引脚,也就是通过此 PIN 就可以检测到 SD 卡是否有插入。UART1_RTS_B 的配置信息分为两部分: MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 和 0x17059。

  MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 的宏定义内容如下:

#define MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x0090 0x031C 0x0000 0x5 0x0

  这 5 个值的含义如下所示:

<mux_reg conf_reg input_reg mux_mode input_val>

  0x0090:mux_reg 寄存器偏移地址,设备树中的 iomuxc 节点就是 IOMUXC 外设对应的节点,根 据 其 reg 属性可知 IOMUXC 外设寄存器起始地址为 0x020e0000 。
  因此,0x020e0000+0x0090=0x020e0090。0x020e0000+mux_reg 就是 PIN 的复用寄存器地址。

  0x031C:conf_reg 寄存器偏移地址,和 mux_reg 一样,0x020e0000+0x031c=0x020e031c,
这个就是寄存器 IOMUXC_SW_PAD_CTL_PAD_UART1_RTS_B 的地址。

  0x0000:input_reg 寄存器偏移地址,有些外设有 input_reg 寄存器,有 input_reg 寄存器的 外设需要配置 input_reg 寄存器。没有的话就不需要设置,UART1_RTS_B 这个 PIN 在做 GPIO1_IO19 的时候是没有 input_reg 寄存器,因此这里 intput_reg 是无效的。

  0x5:mux_reg 寄 存 器 值 , 在 这 里 就 相 当 于 设 置IOMUXC_SW_MUX_CTL_PAD_UART1_RTS_B 寄存器为 0x5,也即是设置 UART1_RTS_B 这个 PIN 复用为 GPIO1_IO19。

  0x0:input_reg 寄存器值,在这里无效。

  0x17059:conf_reg 寄存器值,此值由用户自行设置,通过此值来设置一个 IO 的上/下拉、驱动能力和速度等。在这里就相当于设置寄存器 。IOMUXC_SW_PAD_CTL_PAD_UART1_RTS_B 的值为 0x17059。

在这里插入图片描述

2、gpio 子系统

  如果 pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来就要用到 gpio 子系统了。gpio 子系统用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO 为输入输出,读取 GPIO 的值等。gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API 函数来操作 GPIO,Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开发者使用 GPIO。

(1)设备树中添加 gpio 节点模板

  在根节点“/”下创建 test 设备子节点,pinctrl_test 描述了 test 设备所使用的 GPIO1_IO00 这个 PIN 的信息,我们要将这节点添加到 test 设备节点中,最后需要在 test 节点中添加 GPIO 属性信息,表明 test 所使用的 GPIO 是哪个引脚,如下所示:

test { 
	pinctrl-names = "default"; 
	pinctrl-0 = <&pinctrl_test>;
	gpio = <&gpio1 0 GPIO_ACTIVE_LOW>;
};

属性“gpio”描述了 引脚使用的哪个 IO。属性值一共有三个:

  “&gpio1”表示所使用的 IO 属于 GPIO1 组。

  “0” 表示 GPIO1 组的第 0 号 IO,通过这两个值驱动程序就知道引脚使用了 GPIO1_IO0 这 GPIO。

  “GPIO_ACTIVE_LOW”表示低电平有效,如果改为“GPIO_ACTIVE_HIGH”就表示高电平有效。

二、与 gpio 子系统相关的 OF 函数

1、of_get_named_gpio函数

  此函数获取 GPIO 编号,因为 Linux 内核中关于 GPIO 的 API 函数都要使用 GPIO 编号, 此函数会将设备树中类似<&gpio5 7 GPIO_ACTIVE_LOW>的属性信息转换为对应的 GPIO 编号,函数原型如下:

int of_get_named_gpio(struct device_node *np, const char *propname,int index)

函数参数和返回值含义如下:

  np:设备节点。

  propname:包含要获取 GPIO 信息的属性名。

  index:GPIO 索引,因为一个属性里面可能包含多个 GPIO,此参数指定要获取哪个 GPIO 的编号,如果只有一个 GPIO 信息的话此参数为 0。

  返回值:正值,获取到的 GPIO 编号;负值,失败。

三、gpio 子系统 API 函数

1、gpio_request 函数

  gpio_request 函数用于申请一个 GPIO 管脚,在使用一个 GPIO 之前一定要使用 gpio_request 进行申请,函数原型如下:

int gpio_request(unsigned gpio, const char *label)

函数参数和返回值含义如下:

gpio:要申请的 gpio 标号,使用 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信
息,此函数会返回这个 GPIO 的标号。

label:给 gpio 设置个名字。

返回值:0,申请成功;其他值,申请失败。

2、gpio_free 函数

  如果不使用某个 GPIO 了,那么就可以调用 gpio_free 函数进行释放。函数原型如下:

void gpio_free(unsigned gpio)

函数参数和返回值含义如下:

gpio:要释放的 gpio 标号。

返回值:无。

3、gpio_direction_input函数

  此函数用于设置某个 GPIO 为输入,函数原型如下所示:

int gpio_direction_input(unsigned gpio)

函数参数和返回值含义如下:

  gpio:要设置为输入的 GPIO 标号。

  返回值:0,设置成功;负值,设置失败。

4、gpio_direction_output 函数

此函数用于设置某个 GPIO 为输出,并且设置默认输出值,函数原型如下:

int gpio_direction_output(unsigned gpio, int value)

函数参数和返回值含义如下:

  gpio:要设置为输出的 GPIO 标号。

  value:GPIO 默认输出值。

  返回值:0,设置成功;负值,设置失败。

5、gpio_get_value 函数

  此函数用于获取某个 GPIO 的值(0 或 1),此函数是个宏,定义所示:

#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)

函数参数和返回值含义如下:

  gpio:要获取的 GPIO 标号。

  返回值:非负值,得到的 GPIO 值;负值,获取失败。

6、gpio_set_value 函数

  此函数用于设置某个 GPIO 的值,此函数是个宏,定义如下

#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)

函数参数和返回值含义如下:

  gpio:要设置的 GPIO 标号。

  value:要设置的值。

  返回值:无

四、硬件原理分析

在这里插入图片描述

  通过一个 PNP 型的三极管 8550 来驱动蜂鸣器,通过 SNVS_TAMPER1 这个 IO 来控制三极管 Q1 的导通,当 SNVS_TAMPER1 输出低电平的时候 Q1 导通,相当于蜂鸣器的正极连接到 DCDC_3V3,蜂鸣器形成一个通路,因此蜂鸣器会鸣叫。同理,当 SNVS_TAMPER1 输出高电平的时候 Q1 不导通,那么蜂鸣器就没有形成一个通路,因此蜂鸣器也就不会鸣叫。

五、实验程序编写

1、修改设备树文件

(1)添加 pinctrl 节点

  I.MX6U-ALPHA开发板上的BEEP使用了SNVS_TAMPER1这个PIN,打开imx6ull-alientekemmc.dts,在 iomuxc 节点的 imx6ul-evk 子节点下创建一个名“pinctrl_beep”的子节点,节点内容如下所示:

pinctrl_beep: beepgrp { 
	fsl,pins = < 
		MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x10B0 /* beep */ 
	>;
};

  将 SNVS_TAMPER1 这个 PIN 复用为 GPIO5_IO01 ,宏 MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 定义在 arch/arm/boot/dts/imx6ull-pinfunc-snvs.h文件中。

(2)添加beep设备节点

  在根节点“/”下创建 BEEP 节点,节点名为“beep”,节点内容如下:

beep { 
	#address-cells = <1>;
	#size-cells = <1>;
	compatible = "atkalpha-beep"; 
	pinctrl-names = "default"; 
	pinctrl-0 = <&pinctrl_beep>;
	beep-gpio = <&gpio5 1 GPIO_ACTIVE_HIGH>;
	status = "okay";
}

  设备树编写完成以后使用“make dtbs”命令重新编译设备树,然后使用新编译出来的 imx6ull-alientek-emmc.dtb 文件启动 Linux 系统。启动成功以后进入“/proc/device-tree”目录中查看“beep”节点是否存在,如果存在的话就说明设备树基本修改成功。

在这里插入图片描述

2、蜂鸣器驱动程序编写

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define BEEP_CNT			1		/* 设备号个数 */
#define BEEP_NAME			"beep"	/* 名字 */
#define BEEPOFF 			0		/* 关蜂鸣器 */
#define BEEPON 				1		/* 开蜂鸣器 */


/* beep设备结构体 */
struct beep_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
	struct device_node	*nd; /* 设备节点 */
	int beep_gpio;			/* beep所使用的GPIO编号		*/
};

struct beep_dev beep;		/* beep设备 */

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int beep_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &beep; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t beep_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char beepstat;
	struct beep_dev *dev = filp->private_data;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	beepstat = databuf[0];		/* 获取状态值 */

	if(beepstat == BEEPON) {	
		gpio_set_value(dev->beep_gpio, 0);	/* 打开蜂鸣器 */
	} else if(beepstat == BEEPOFF) {
		gpio_set_value(dev->beep_gpio, 1);	/* 关闭蜂鸣器 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int beep_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations beep_fops = {
	.owner = THIS_MODULE,
	.open = beep_open,
	.write = beep_write,
	.release = 	beep_release,
};

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init beep_init(void)
{
	int ret = 0;

	/* 设置BEEP所使用的GPIO */
	/* 1、获取设备节点:beep */
	beep.nd = of_find_node_by_path("/beep");
	if(beep.nd == NULL) {
		printk("beep node not find!\r\n");
		return -EINVAL;
	} else {
		printk("beep node find!\r\n");
	}

	/* 2、 获取设备树中的gpio属性,得到BEEP所使用的BEEP编号 */
	beep.beep_gpio = of_get_named_gpio(beep.nd, "beep-gpio", 0);
	if(beep.beep_gpio < 0) {
		printk("can't get beep-gpio");
		return -EINVAL;
	}
	printk("led-gpio num = %d\r\n", beep.beep_gpio);

	/* 3、设置GPIO5_IO01为输出,并且输出高电平,默认关闭BEEP */
	ret = gpio_direction_output(beep.beep_gpio, 1);
	if(ret < 0) {
		printk("can't set gpio!\r\n");
	}

	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (beep.major) {		/*  定义了设备号 */
		beep.devid = MKDEV(beep.major, 0);
		register_chrdev_region(beep.devid, BEEP_CNT, BEEP_NAME);
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&beep.devid, 0, BEEP_CNT, BEEP_NAME);	/* 申请设备号 */
		beep.major = MAJOR(beep.devid);	/* 获取分配号的主设备号 */
		beep.minor = MINOR(beep.devid);	/* 获取分配号的次设备号 */
	}
	printk("beep major=%d,minor=%d\r\n",beep.major, beep.minor);	
	
	/* 2、初始化cdev */
	beep.cdev.owner = THIS_MODULE;
	cdev_init(&beep.cdev, &beep_fops);
	
	/* 3、添加一个cdev */
	cdev_add(&beep.cdev, beep.devid, BEEP_CNT);

	/* 4、创建类 */
	beep.class = class_create(THIS_MODULE, BEEP_NAME);
	if (IS_ERR(beep.class)) {
		return PTR_ERR(beep.class);
	}

	/* 5、创建设备 */
	beep.device = device_create(beep.class, NULL, beep.devid, NULL, BEEP_NAME);
	if (IS_ERR(beep.device)) {
		return PTR_ERR(beep.device);
	}
	
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit beep_exit(void)
{
	/* 注销字符设备驱动 */
	cdev_del(&beep.cdev);/*  删除cdev */
	unregister_chrdev_region(beep.devid, BEEP_CNT); /* 注销设备号 */

	device_destroy(beep.class, beep.devid);
	class_destroy(beep.class);
}

module_init(beep_init);
module_exit(beep_exit);
MODULE_LICENSE("GPL");

3、编写测试APP

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#define BEEPOFF 	0
#define BEEPON 	1

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char databuf[1];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开beep驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 */

	/* 向/dev/beep文件写入数据 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("BEEP Control Failed!\r\n");
		close(fd);
		return -1;
	}

	retvalue = close(fd); /* 关闭文件 */
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

六、运行测试

1、编译驱动程序和测试APP

(1)编译驱动程序

  编写 Makefile 文件, obj-m 变量的值为 beep.o,Makefile 内容如下所示:

KERNELDIR := /home/sh/Desktop/MUL/zimage
CURRENT_PATH := $(shell pwd)
obj-m := beep.o

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-

build: kernel_modules

kernel_modules:
	$(MAKE) -j32 -C $(KERNELDIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -j32 -C $(KERNELDIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) M=$(CURRENT_PATH) clean

  输入如下命令编译出驱动模块文件:

make

  编译成功以后就会生成一个名为“beep.ko”的驱动模块文件。

(2)编译测试APP

  输入如下命令编译测试 beepApp.c 这个测试程序:

arm-linux-gnueabihf-gcc beepApp.c -o beepApp

  编译成功以后就会生成 beepApp 这个应用程序。

2、运行测试

  将 beep.ko 和 beepApp 这两个文件拷贝到开发板,输入如下命令加载 beep.ko 驱动模块:

insmod beep.ko   //加载驱动

  驱动加载成功以后会在终端中输出一些信息,如下图所示:

在这里插入图片描述

  输入如下命令打开蜂鸣器:

/beepApp /dev/beep 1 //打开蜂鸣器

  关闭蜂鸣器:

./beepApp /dev/beep 0 //关闭蜂鸣器

  如果要卸载驱动的话输入如下命令即可:

rmmod beep.ko

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一盆电子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值