嵌入式linux驱动开发-设备树点灯(附加子系统)

pinctrl&gpio子系统

pinctrl&gpio都属于设备树的一个“树枝”节点,用来更系统便捷地操作IO口。
使内核源码更好的适应芯片。

传统设备树控制IO:reg 属 性中<各个寄存器地址…>

添加子系统后的设备树:
(1)pinctrl:设置 PIN 的复用功能、电气特性(速度、驱动能力、上下拉等)。
(2)gpio:设置 GPIO为输入/输出,电平特性。

因为设备树就是描述板级信息的,为驱动文件提供外设需要的PIN,驱动文件只需要管好自己的驱动代码即可,PIN的复用和电气特性啥的 iomuxc 驱动会自动初始化,所以这又进一步封装了,其根本就是为了提高系统的可以移植性和兼容性。

pinctrl子系统

pinctrl 子系统源码目录为 drivers/pinctrl。

1、 imx6ull.dtsi 文件中一个节点iomuxc: iomuxc@020e0000。
2、基本上也是在imx6ull-alientek-emmc.dts中创建一个节点追加,不同的外设描述自己的PIN,如hog_1设备:

	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
		>;	
	};

(可以在设备树中引用 C 语言中.h 文件中的内容。)
解析:

MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059
对应
0x0090 0x031C 0x0000 0x5 0x0	  0x17059
对应
<mux_reg conf_reg input_reg mux_mode input_val>	conf_val

mux_reg 	复用寄存器偏移地址
conf_reg 	配置寄存器偏移地址
input_reg 	input寄存器偏移地址(个别PIN会用到)
mux_mode 	复用寄存器值
input_val	input寄存器值
conf_val	配置寄存器值	

pinctrl关于 PIN 配置入口函数,用来获取相关的属性信息:
在这里插入图片描述

添加pinctrl节点模板

接下来实际操作一下如何添加pinctrl设备:

pinctrl 设 备 树 绑 定 信 息 可 以 参 考 文 档
Documentation/devicetree/bindings/pinctrl/fsl,imx-pinctrl.txt。

1、创建对应的节点
imx6ull-alientek-emmc.dts文件,iomuxc 节点中的“imx6ul-evk”子节点下,添加“pinctrl_test”节点,注意!节点前缀一定要为“pinctrl_”。

2、添加“fsl,pins”属性
属性名字一定要为“fsl,pins”,pinctrl 驱动程序是通过读取“fsl,pins”属性值来获取 PIN 的配置信息。

3、在“fsl,pins”属性中添加 PIN 配置信息

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

gpio子系统

gpio 子系统用于初始化 GPIO 并且提供相应的 API 函数,Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开发者使用 GPIO。

同样是imx6ull-alientek-emmc.dts 中的节点,如:

760 &usdhc1 {
761 	pinctrl-names = "default", "state_100mhz", "state_200mhz";
762 	pinctrl-0 = <&pinctrl_usdhc1>;
763 	pinctrl-1 = <&pinctrl_usdhc1_100mhz>;
764 	pinctrl-2 = <&pinctrl_usdhc1_200mhz>;
765 	/* pinctrl-3 = <&pinctrl_hog_1>; */
766 	cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;
767 	keep-power-in-suspend;
768 	enable-sdio-wakeup;
769 	vmmc-supply = <&reg_sd1_vmmc>;
770 	status = "okay";
771 };

这个就是具体usdhc1的gpio设备节点,其中

pinctrl-0 = <&pinctrl_usdhc1>;

就是调用了pinctrl_那边儿的标签,实现 复用、电气GPIO设置 的对接。

属性“cd-gpios”描述了 SD 卡的 CD 引脚使用的哪个 IO。

cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;

&gpio1”表示 CD 引脚所使用的 IO 属于 GPIO1 组,
“19”表示 GPIO1 组的第 19 号 IO,通过这两个值 SD 卡驱动程序就知道 CD 引脚使用了 GPIO1_IO19这 GPIO。
“GPIO_ACTIVE_LOW”表示低电平有效,如果改为“GPIO_ACTIVE_HIGH”就表
示高电平有效。

gpio 子系统 API 函数

gpio 子系统向驱动开发人员屏蔽了具体的读写寄存器过程。 配置完设备树之后, 我们只需要乖乖使用API函数就行了, 这就是驱动分层与分离的好处,大家各司其职,做好自己的本职工作即可。

1、 gpio_request 函数

用于申请一个 GPIO 管脚。

int gpio_request(unsigned gpio, const char *label)

gpio:要申请的 gpio 标号,使用 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信息,此函数会返回这个 GPIO 的标号。
label:给 gpio 设置个名字。
返回值: 0,申请成功;其他值,申请失败。

2、 gpio_free 函数

用于释放一个 GPIO 管脚。

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: 要设置的值。
返回值: 无

添加 gpio 节点模板

创建 test 设备的 GPIO 节点。

1、创建 test 设备节点。
2、添加 pinctrl 信息。
3、添加 GPIO 属性信息。

1 test {
2 		pinctrl-names = "default";
3 		pinctrl-0 = <&pinctrl_test>;
4 		gpio = <&gpio1 0 GPIO_ACTIVE_LOW>;
5 };

与 gpio 相关的 OF 函数

Linux 内核提供了几个与 GPIO 有关的 OF 函数,可在驱动程序中调用。

1、 of_gpio_named_count 函数

用于获取设备树某个属性里面定义了几个 GPIO 信息,要注意的
是空的 GPIO 信息也会被统计到。

int of_gpio_named_count(struct device_node *np, const char *propname)

np:设备节点。
propname:要统计的 GPIO 属性。
返回值: 正值,统计到的 GPIO 数量;负值,失败。

2、 of_gpio_count 函数

专门统计“gpios”这个属性的 GPIO 数量。

int of_gpio_count(struct device_node *np)

np:设备节点。
返回值: 正值,统计到的 GPIO 数量;负值,失败。

3、 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 编号;负值,失败。

实验程序编写

没有封装的年代:通过设备树向 dtsled.c 文件传递相应的寄存器物理地址,然后在驱动文件中配置寄存器。

有了封装的年代:使用 pinctrl 和 gpio 子系统来完成 LED 灯驱动。

修改设备树文件

1、添加 pinctrl_ 节点(pinctrl子系统

imx6ull-alientekemmc.dts文件下, iomuxc 节点–> imx6ul-evk 子节点–>pinctrl_led的子节点
GPIO1_IO03 这个 PIN 复用为 GPIO1_IO03,电气属性值为 0X10B0

1 pinctrl_led: ledgrp {
2 		fsl,pins = <
3 			MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /* LED0 */
4 		>;
5 };

2、添加 LED 设备节点(gpio子系统

根节点“/”–>节点gpioled( LED 灯节点):
led-gpio 属性指定了 LED 灯所使用的 GPIO,在这里就是 GPIO1 的 IO03,低电平有效。
gpio 子系统的 API 操作函数需要 GPIO 编号

1 gpioled {
2 		#address-cells = <1>;
3 		#size-cells = <1>;
4 		compatible = "atkalpha-gpioled";
5 		pinctrl-names = "default";
6 		pinctrl-0 = <&pinctrl_led>;		//使用上边儿pinctrl_描述的复用和电气属性 pinctrl-0 属性设置 LED 灯所使用的 PIN 对应的 pinctrl 节点
7 		led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
8 		status = "okay";
9 }

3、检查 PIN 是否被其他外设使用

这一点非常重要!!!

如果 A 引脚在设备树中配置为了 I2C 的 SDA 信号,那么 A 引脚就不能再配置为 GPIO,否则的话驱动程序在申请 GPIO 的时候就会失败。(hai~剪不断,理还乱)

包括两个方面的检查:
①、检查 pinctrl 设置。
②、如果这个 PIN 配置为 GPIO 的话,检查这个 GPIO 有没有被别的外设使用。

这不巧了吗!!使用的LED灯GPIO1_IO3正好被NXP厂商的TSC电阻触摸屏接口占用了,幸运的是,原子的板子并没有使用这种外设屏幕,pinctrl和gpio的两个子系统全部注释屏蔽掉好了。
其他的找到一个屏蔽一个有关GPIO1_IO3引脚的。

最后,重新编译设备树:

make dtbs
//进入系统后看看有没有节点
cd /proc/device-tree

在这里插入图片描述

LED 灯驱动程序编写

gpioled.c

#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>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: gpioled.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: 采用pinctrl和gpio子系统驱动LED灯。
其他	   	: 无
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/7/13 左忠凯创建
***************************************************************/
#define GPIOLED_CNT			1		  	/* 设备号个数 */
#define GPIOLED_NAME		"gpioled"	/* 名字 */
#define LEDOFF 				0			/* 关灯 */
#define LEDON 				1			/* 开灯 */

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

struct gpioled_dev gpioled;	/* led设备 */

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

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;
	/*声明一个自己写的设备结构体的指针 指向打开文件时的私有变量,里边儿有led设备结构体变量*/
	struct gpioled_dev *dev = filp->private_data;

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

	ledstat = databuf[0];		/* 获取状态值 */
	//用到了linux内核函数GPIO函数 调用led设备结构体变量中的gpio编号
	if(ledstat == LEDON) {	
		gpio_set_value(dev->led_gpio, 0);	/* 打开LED灯 */
	} else if(ledstat == LEDOFF) {
		gpio_set_value(dev->led_gpio, 1);	/* 关闭LED灯 */
	}
	return 0;
}

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

/* 设备操作函数 */
static struct file_operations gpioled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
};

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

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

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

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

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

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

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

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

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

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

驱动程序总结:
以上的驱动程序和上一章中普通设备树的驱动差不多,只不过将设备GPIO初始化的一大部分释放给了设备树去完成,改为使用 Linux 内核提供的 API 函数,IO口在以后的开发中会用到很多,因此这样能更高效。
通过读取 filp 的 private_data 私有成员变量来得到设备结构体变量,也就是 gpioled。

再来总结一下驱动程序框架:

1、一些参数准备,设备结构体定义和声明等。

2、file_opreation结构体变量声明部分

3、驱动入口函数
	(1)获取设备树中属性值
	(2)设备初始化
	(3)设备号、cdev、class、device等注册

4、驱动出口函数
	(1)设备号、cdev、class、device等注销

测试 APP

同前边儿几章。

Makefile

同前边儿几章。

运行测试

都在 设备树点灯(完备版) 有详细说明。

为完成“天梯计划”,是时候学学图像识别了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值