Linux下设备树、pinctrl和gpio子系统、LED灯驱动实验

设备树

描述设备树的文件叫做DTS(Device Tree Source),这个DTS文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如CPU数量、 内存基地址、IIC接口上接了哪些设备、SPI接口上接了哪些设备等等。其中,树的主干是系统总线。
没有设备树之前文件很多,每个文件负责描述一部分功能,显得很杂乱,设备树文件就将这些描述板级信息的文件整合起来,用一个专属文件格式来描述,文件扩展名为.dts。一个SOC可以作出很多不同的板子,这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的.dts文件直接引用这个通用文件即可,这个通用文件就是.dtsi(device tree source include)文件,类似于C语言中的头文件。一般.dts描述板级信息,比如开发板上有哪些IIC设备、SPI设备等,.dtsi描述SOC级信息,也就是SOC有几个CPU、主频是多少、各个外设控制器信息等。
将.c文件编译为.o需要用到gcc编译器,将.dts编译为.dtb需要DTC工具,DTC工具源码在Linux内核的scripts/dtc目录下。要编译DTS文件只需进入到Linux源码根目录下,执行make dtbs命令即可。make all是编译Linux源码中的所有文件,包括zImage、.ko驱动以及设备树文件等。
设备树中节点命名格式:nodename@address
设备树中节点命名格式也可以是:label:nodename@address
引入label的目的就是为了方便访问节点,上面例子可以直接通过&label来访问这个节点。
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。
头文件之后以 / 开始的是根节点。
compatible属性是兼容性属性,其格式compatible =“manufacturer,model”,manufacturer表示厂商,model是模块对应的驱动名字。

compatible = “fsl,imx6ul-evk-wm8960”,“fsl,imx-audio-wm8960”;

上面的compatible有两个属性,首先使用第一个,如果在Linux里找不到与之匹配的驱动文件,就使用第二个。
一般驱动程序文件都会有一个OF匹配表,此OF匹配表保存着一些compatible值,如果设备节点的compatible属性值和OF匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。
根节点的compatible属性中,第一个值描述了所使用的硬件设备名字,第二个值描述了设备所使用的SOC。

compatible = “fsl,imx6dl-sabresd”, “fsl,imx6dl”;

Linux内核会通过根节点的compoatible属性查看是否支持此设备,如果支持的话设备就会启动Linux内核。
model 属性值是一个字符串,一般model属性描述设备模块信息,如model = “wm8960-audio”;
status属性是设备的状态,其值也是字符串,字符串是设备的状态信息,有“okay”、“disabled”、“fail”、“fail-sss”。“okay”表示设备可操作;“disabled”表示设备当前不可操作,但是在未来可以变成可操作的;“fail”表示设备不可操作;“fail-sss”不可操作,sss表示检测到的错误。
#address-cells和#size-cells属性值都是无符号32位整形,这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息,分别表示子节点reg属性中地址/长度信息所占的字长。

spi4 {
    
    
	compatible = "spi-gpio";
	#address-cells = <1>;
	#size-cells = <0>;
	
	gpio_spi: gpio_spi@0 {
    
    
		compatible = "fairchild,74hc595";
		reg = <0>;
	 };
};

上面描述的起始地址所占用的字长为1,地址长度所占用的字长为0,子节点gpio_spi的reg属性值为0,所以起始地址为0,没有设置地址长度。

aips3: aips-bus@02200000 {
    
    
	compatible = "fsl,aips-bus", "simple-bus";
	#address-cells = <1>;
	#size-cells = <1>;
	
	dcp: dcp@02280000 {
    
    
		compatible = "fsl,imx6sl-dcp";
		reg = <0x02280000 0x4000>;
	};
};

上面的代码相当于设置了起始地址为0x02280000,地址长度为0x4000。
reg属性的值一般是(address,length)对,reg属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息。
ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵,ranges是一个地址映射/转换表,ranges属性每个项目由子地址、父地址和地址空间长度这三部分组成。如果ranges属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。
向节点追加或修改内容,不能在设备树头文件(.dtsi)中的该节点下直接修改,因为设备树头文件可能被多个板子引用,直接添加的话就相当于给其他所有的板子都添加了,其他的板子不一定有这个设备,因此添加或者修改的重点就是在需要添加设备的设备树文件(.dtb)中通过&label来访问节点,然后直接在里面编写要追加或者修改的内容。
在/proc/device-tree目录下通过cat就可以查看代码中定义的信息。
在这里插入图片描述
设备树中的代码设置如下,其和开发板查看到的一致。
在这里插入图片描述
Linux内核在启动的时候会解析dtb文件,然后在/proc/device-tree目录下生成相应的设备树节点文件。
在这里插入图片描述
在设备树中添加一个硬件对应的节点的时候需要到Linux源码目录/Documentation/devicetree/bindings目录下根据自己添加的类型选择相应的文件夹,进去之后找到对应的文本文档打开就有添加示例,如下图所示是/i2c/i2c-imx.txt,例子是如何在设备树中添加I2C设备节点。
在这里插入图片描述


常用的of函数

Linux内核提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of”,所以在很多资料里面也被叫做of函数,这些of函数原型都定义在include/linux/of.h文件中。
查找节点有关的of函数
of_find_node_by_name函数通过节点名字查找指定的节点,from是开始查找的节点,为NULL表示从根节点开始查找,name是要查找的节点名字,返回值是找到的节点,为NULL表示查找失败。

struct device_node *of_find_node_by_name(struct device_node *from,const char *name)

of_find_node_by_type函数通过device_type属性查找指定的节点,type是要查找的节点对应的type字符串。

struct device_node *of_find_node_by_type(struct device_node *from, const char *type)

of_find_compatible_node函数根据device_type和compatible这两个属性查找指定的节点,type可以为NULL,表示忽略掉device_type属性,compatible是要查找的节点所对应的compatible属性列表。

struct device_node *of_find_compatible_node(struct device_node *from,const char *type,const char *compatible)

of_find_matching_node_and_match函数通过of_device_id匹配表来查找指定的节点,matches是of_device_id匹配表,也就是在此匹配表里面查找节点,match表示找到的匹配的of_device_id。

struct device_node *of_find_matching_node_and_match(struct device_node *from,const struct of_device_id *matches,const struct of_device_id **match)

of_find_node_by_path函数通过路径来查找指定的节点,path是带有全路径的节点名,是设备树中定义的节点名称,可以使用节点的别名。

inline struct device_node *of_find_node_by_path(const char *path)

查找父子节点的of函数
of_get_parent函数用于获取指定节点的父节点,如果有的话,node是要查找父节点的节点,返回值是找到的父节点。

struct device_node *of_get_parent(const struct device_node *node)

of_get_next_child函数用迭代的查找子节点,node是父节点,prev是前一个子节点,可设置为NULL,表示从第一个子节点开始,返回值是找到的下一个子节点。

struct device_node *of_get_next_child(const struct device_node *node,struct device_node *prev)

提取属性值的of函数
of_find_property 函数用于查找指定的属性,np是设备节点,name是属性名字,lenp是属性值的字节数。

property *of_find_property(const struct device_node *np,const char *name,int *lenp)

of_property_count_elems_of_size 函数用于获取属性中元素的数量,比如reg 属性值是一个数组,那么使用此函数可以获取到这个数组的大小。proname是需要统计元素数量的属性名字,elem_size是元素长度,返回值是得到的属性元素数量。

int of_property_count_elems_of_size(const struct device_node *np,const char *propname,int elem_size)

of_property_read_u32_index 函数用于从属性中获取指定标号的u32 类型数据值(无符号32位),比如某个属性有多个u32 类型的值,那么就可以使用此函数来获取指定标号的数据值。index是要读取的值标号,out_value是读取到的值,返回值为0表示读取成功。

int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index,u32 *out_value)

这4个函数分别是读取属性中u8、u16、u32 和u64 类型的数组数据,比如大多数的reg属性都是数组数据,可以使用这4个函数一次读取出reg属性中的所有数据。sz是要读取的数组元素数量。

int of_property_read_u8_array(const struct device_node *np,const char *propname,u8 *out_values,size_t sz)
int of_property_read_u16_array(const struct device_node *np,const char *propname,u16 *out_values,size_t sz)
int of_property_read_u32_array(const struct device_node *np,const char *propname,u32 *out_values,size_t sz)
int of_property_read_u64_array(const struct device_node *np,const char *propname,u64 *out_values,size_t sz)

这4个函数就是用于读取只有一个整型值的属性,分别用于读取u8、u16、u32 和u64类型属性值。

int of_property_read_u8(const struct device_node *np,const char *propname,u8 *out_value)
int of_property_read_u16(const struct device_node *np,const char *propname,u16 *out_value)
int of_property_read_u32(const struct device_node *np,const char *propname,u32 *out_value)
int of_property_read_u64(const struct device_node *np,const char *propname,u64 *out_value)

of_property_read_string函数用于读取属性中字符串值,proname是要读取的属性名字,out_string是读取到的字符串值,返回值为0表示读取成功。

int of_property_read_string(struct device_node *np,const char *propname,const char **out_string)

of_n_addr_cells函数用于获取#address-cells属性值,返回值是获取到的#address-cells属性值。

int of_n_addr_cells(struct device_node *np)

of_size_cells函数用于获取#size-cells属性值,返回值是获取到的#size-cells属性值。

int of_n_size_cells(struct device_node *np)

常用的of函数
of_device_is_compatible函数用于查看节点的compatible属性是否有包含compat指定的字符串,也就是检查设备节点的兼容性。

int of_device_is_compatible(const struct device_node *device,const char *compat)

device是设备节点,compat是要查看的字符串,返回值为0表示节点的compatible属性中不包含compat指定的字符串,返回值为正数表示节点的compatible属性中包含compat指定的字符串。
of_get_address函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值。

const __be32 *of_get_address(struct device_node *dev,int index,u64 *size,unsigned int *flags)

dev是设备节点,index是要读取的地址标号,size是地址长度,flags是参数,比如IORESOURCE_IO、IORESOURCE_MEM等,返回值是读取到的地址数据首地址,为NULL表示读取失败。
of_translate_address 函数负责将从设备树读取到的地址转换为物理地址。

u64 of_translate_address(struct device_node *dev,const __be32 *in_addr)

dev是设备节点,in_addr是要转换的地址,返回值是得到的物理地址,如果为OF_BAD_ADDR的话表示转换失败。
of_address_to_resource是将reg属性值转换为resource结构体类型。

int of_address_to_resource(struct device_node *dev,int index,struct resource *r)

dev是设备节点,index是地址资源标号,r表示得到的resource类型的资源值。返回值为0表示成功,负值表示失败。
of_iomap函数用于直接内存映射

void __iomem *of_iomap(struct device_node *np,int index)

np是设备节点,index是reg属性中要完成内存映射的段,如果reg属性只有一段的话index就设置为0,返回值是经过内存映射后的虚拟内存首地址,如果为NULL的话表示内存映射失败。


pinctrl子系统

Linux驱动讲究驱动分离与分层,pinctrl和gpio子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架。Linux内核针对PIN的配置推出了pinctrl子系统,对于GPIO的配置推出了gpio子系统。
pinctrl子系统重点是设置PIN(有的SOC叫做PAD)的复用和电气属性。
传统的配置pin的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如pin功能冲突),pinctrl子系统就是为了解决这个问题而引入的,pinctrl子系统主要工作内容是:①获取设备树中的pin信息;②根据获取到的pin信息来设置pin的复用功能;③根据获取到的pin信息来设置pin的电气特性,比如上/下拉、速度、驱动能力等。
对于使用者来讲,只需要在设备树里面设置好某个pin的相关属性即可,其他的初始化工作均由pinctrl子系统来完成,pinctrl子系统源码目录为/drivers/pinctrl。设备树中,iomuxc节点就是外设对应的节点。
compatible属性值为“fsl,imx6dl-iomuxc”,Linux内核会根据compatbile属性值来查找对应的驱动文件,所以在Linux内核源码中全局搜索字符串“fsl,imx6dl-iomuxc”就会找到I.MX6DL这颗SOC的pinctrl驱动文件。
驱动文件在路径/drivers/pinctrl/freescale/pinctrl-imx6dl.c中。

static const struct of_device_id imx6dl_pinctrl_of_match[] = {
    
    
	{
    
     .compatible = "fsl,imx6dl-iomuxc", },
	{
    
     /* sentinel */ }
};

static int imx6dl_pinctrl_probe(struct platform_device *pdev)
{
    
    
	return imx_pinctrl_probe(pdev, &imx6dl_pinctrl_info);
}

static struct platform_driver imx6dl_pinctrl_driver = {
    
    
	.driver = {
    
    
		.name = "imx6dl-pinctrl",
		.of_match_table = imx6dl_pinctrl_of_match,
	},
	.probe = imx6dl_pinctrl_probe,
	.remove = imx_pinctrl_remove,
};

/arch/arm/boot/dts/imx6dl-pinfunc.h文件中定义了很多复用的宏定义。
在imx6dl-pinfunc.h文件中找到EIM_A19对应的所有宏定义如下。

#define MX6QDL_PAD_EIM_A19__EIM_ADDR19              0x11c 0x4ec 0x000 0x0 0x0
#define MX6QDL_PAD_EIM_A19__IPU1_DISP1_DATA14       0x11c 0x4ec 0x000 0x1 0x0
#define MX6QDL_PAD_EIM_A19__IPU1_CSI1_DATA14        0x11c 0x4ec 0x898 0x2 0x0
#define MX6QDL_PAD_EIM_A19__GPIO2_IO19              0x11c 0x4ec 0x000 0x5 0x0
#define MX6QDL_PAD_EIM_A19__SRC_BOOT_CFG19          0x11c 0x4ec 0x000 0x7 0x0
#define MX6QDL_PAD_EIM_A19__EPDC_PWR_CTRL1          0x11c 0x4ec 0x000 0x8 0x0

在数据手册中搜索EIM_A19找到对应的位置,如下图。
在这里插入图片描述
imx6dl.dtsi代码中定义的iomuxc节点如下,其起始地址为0x020e0000。

iomuxc: iomuxc@020e0000 {
    
    
	compatible = "fsl,imx6dl-iomuxc";
};

宏定义中后面5个值的含义如下。

<mux_reg conf_reg input_reg mux_mode input_val>

0x020e0000+mux_reg是PIN的复用寄存器地址;conf_reg寄存器的偏移地址;input_reg寄存器的偏移地址,有些外设有input_reg寄存器,有input_reg寄存器的外设需要配置input_reg寄存器,没有的话就不需要设置;mux_mode是mux_reg寄存器值的值,是根据上面表中的MUX_MODE选择的,比如0x5就代表GPIO2_IO19;input_val是input_reg寄存器的值。
添加pinctrl节点的节点前缀一定是"pinctrl_",设备树是通过属性来保存信息的,在这里添加一个名为"fsl,pins"的属性,因为pinctrl 驱动程序是通过读取“fsl,pins”属性值来获取PIN的配置信息的。
下面的就是一个简单的pinctrl节点添加。

pinctrl_led19: uart3{
    
      //开发板上uart3对应的gpio
     fsl,pins = <
         MX6QDL_PAD_EIM_A19__GPIO2_IO19     0x80000000
      >;
};

gpio子系统

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

int gpio_request(unsigned gpio, const char *label)

gpio:要申请的gpio 标号,使用of_get_named_gpio函数从设备树获取指定GPIO属性信息,此函数会返回这个GPIO的标号。
label:给gpio设置的名字。
返回值为0就表示申请成功。
gpio_free函数对不使用的GPIO进行释放。

void gpio_free(unsigned gpio)

gpio_direction_input函数用于设置某个GPIO为输入,返回值为0就表示申请成功。

int gpio_direction_input(unsigned gpio)

gpio_direction_output函数用于设置某个GPIO为输出,value为设置的GPIO的默认输出值,返回值为0就表示设置成功。

int gpio_direction_output(unsigned gpio, int value)

gpio_get_value函数用于获取某个GPIO的值(0或1),此函数是个宏,具体定义如下。

#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)

返回值为非负值就得到了GPIO的值,为负值就表示获取失败。
gpio_set_value函数用于设置某个GPIO的值,此函数是个宏,具体定义如下。

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

其中的value参数就是要设置的值。
在/include/dt-bindings/下有引用的gpio和input等头文件。
关于gpio节点的定义在设备树im6qdl.dtsi中。

gpio1: gpio@0209c000 {
    
    
	compatible = "fsl,imx6q-gpio", "fsl,imx35-gpio";
	reg = <0x0209c000 0x4000>;
	interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>,
			 <0 67 IRQ_TYPE_LEVEL_HIGH>;
	gpio-controller;    //表示gpio1节点是个GPIO控制器
	#gpio-cells = <2>; //表示共两个cell,第一个cell为GPIO编号,比如“&gpio1 3”就表示GPIO1_IO03,第二个cell为极性,如果为0(GPIO_ACTIVE_HIGH) 的话表示高电平有效, 如果为1(GPIO_ACTIVE_LOW)的话表示低电平有效
	interrupt-controller;
	#interrupt-cells = <2>;
};

查看手册,gpio1的起始地址就是0x0209c000,如下。
在这里插入图片描述
设备树根目录下添加下面的内容。

gpio_led{
    
     
	#address-cells = <1>;
    #size-cells = <1>;
    pinctrl-names = "default";
	compatible = "gpio_uart_led";
	pinctrl-0 = <&pinctrl_led>;   //设置LED灯所使用的PIN对应的pinctrl节点
    led-gpio = <&gpio2 19 GPIO_ACTIVE_LOW>;  //指定了LED灯所使用的GPIO,低电平有效,驱动程序会获取led-gpio属性的内容来得到GPIO编号
    status = "okay";
};     

和上面的pinctrl_led19共同完成了pinctrl-gpio的设备树设置。


LED灯驱动实验

LED灯的驱动实验在IMX6DL开发板上进行,因为我没有在开发板上找到可供用户使用的LED,所以本实验点亮的LED是通过开发板uart口外接的RS232上的一个LED。

修改设备树文件

P16是开发板上的uart2,其原理图如下,可以复用EIM_A17。
在这里插入图片描述
EIM_A18按理说应该也可以复用的,但是我在实验时加载卸载驱动什么的都正常,但是灯就是不亮。
P17是开发板上的uart3,其原理图如下,可以复用EIM_A19。
在这里插入图片描述
EIM_A17和EIM_A19分别对应uart2和uart3口,我经过实验都是可以点亮LED的。
Linux内核下/arch/arm/boot/dts/imx6dl-pinfunc.h中关于EIM_A17和EIM_A19复用为GPIO的宏定义如下。

#define MX6QDL_PAD_EIM_A17__GPIO2_IO21              0x114 0x4e4 0x000 0x5 0x0
#define MX6QDL_PAD_EIM_A19__GPIO2_IO19              0x11c 0x4ec 0x000 0x5 0x0

可以看到,EIM_A17复用为第二组GPIO的21号脚,EIM_A19复用为第二组GPIO的19号脚,这些信息在设备树中定义pinctrl时会用到,具体的代码如下。

pinctrl_led:uart3{
    
      
     fsl,pins = <
              //MX6QDL_PAD_EIM_A19__GPIO2_IO19     0x80000000    //将EIM_A19复用为GPIO2_IO19
              MX6QDL_PAD_EIM_A17__GPIO2_IO21     0x80000000    //将EIM_A17复用为GPIO2_IO21
     >;
};

此外还要在设备树的根目录下定义节点信息,代码如下。

gpio_led{
    
     
	#address-cells = <1>;
    #size-cells = <1>;
    pinctrl-names = "default";
	compatible = "gpio_uart_led";
	pinctrl-0 = <&pinctrl_led>;   //设置LED灯所使用的PIN对应的pinctrl节点
    //led-gpio = <&gpio2 19 GPIO_ACTIVE_LOW>;  //uart3指定了LED灯所使用的GPIO,低电平有效,驱动程序会获取led-gpio属性的内容来得到GPIO编号
    led-gpio = <&gpio2 21 GPIO_ACTIVE_LOW>;  //uart2口,复用EIM_A17为GPIO2_21
    status = "okay";
};  

上面的代码中将EIM_A17和EIM_A19的复用代码都添加上了,测试哪个就暂时把不用的那一个的代码注释掉,防止出差错。
上面的两段代码添加到/arch/arm/boot/dts/imx6dl-c-sabresd.dts文件中,对于本实验来说,设备树文件就已经修改好了。
然后在Linux文件下编译设备树,可以只编译修改的imx6dl-c-sabresd.dts文件或者编译所有设备树文件,命令如下。

make imx6dl-c-sabresd.dtb
make dtbs

编译完成后,可以将该设备树文件直接烧写到开发板中,这种方法耗时而且不高效,尤其对于调试程序来说,一般都是在开发板启动的过程中,将镜像文件zImage和imx6dl-c-sabresd.dtb发送到开发板启动的,具体的方法可参考文章Linux下通过tftp烧写设备树文件并启动开发板

编写驱动代码

该代码来自正点原子,本人只做了一点改动!
cdev结构体定义在/include/linux/cdev.h文件中,定义如下。

struct cdev {
    
    
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};

在Linux中使用cdev结构体表示一个字符设备,编写字符设备驱动之前需要定义一个cdev结构体变量,这个变量就表示一个字符设备。
led.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>
#include <linux/platform_device.h> 

#define NODE_NAME "gpioled"

dev_t devid;			   // 设备号
struct cdev cdev;		   // 字符设备
struct class *class;	   // 类 
struct device *device;	   //  设备 
struct device_node	*nd;  // 设备节点
int major;				  // 主设备号
int minor;				  // 次设备号
int led_gpio;			 // led所使用的GPIO编号

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    
    
	int ret;
	char kbuf[1];

	ret = copy_from_user(kbuf,buf,cnt);   //将用户写入的数值传到内核
	if(ret < 0) 
    {
    
    
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}
	
	if(kbuf[0] == 1) 
    {
    
    	
		gpio_set_value(led_gpio,1);	   //给gpio写入1,打开LED
        printk("The led is turned on!\n");
	} 
    else
    {
    
    
		gpio_set_value(led_gpio,0);	  //给gpio写入0,关闭LED
        printk("The led is turned off!\n");
	}
	return 0;
}

//设备操作函数 
static struct file_operations gpioled_fops = {
    
    
	.owner = THIS_MODULE,
	.write = led_write
};

const struct of_device_id of_match_table_led[] = {
    
    
	{
    
    .compatible = "gpio_uart_led"},        //与设备树中的compatible属性匹配
	{
    
    }
};

struct platform_driver dts_device = {
    
        
	.driver = {
    
    
		.owner = THIS_MODULE,
		.name = "led",   //目录/sys/bus/platform/drivers/下的驱动文件名
		.of_match_table = of_match_table_led
	}
};

static int __init led_init(void)
{
    
    
	int ret = 0;
	nd = of_find_node_by_path("/gpio_led");   //设备树下的节点名
	if(nd == NULL) 
    {
    
    
		printk("gpio_led node not find!\r\n");
		return -EINVAL;
	} 
    else 
		printk("gpio_led node find!\r\n");

	led_gpio = of_get_named_gpio(nd, "led-gpio", 0);
	if(led_gpio < 0) 
    {
    
    
		printk("of_get_named_gpio error!\r\n");
		return -EINVAL;
	}
	else
		printk("led-gpio num = %d\r\n",led_gpio);
	
    ret = gpio_request(led_gpio,"led");   //申请GPIO 
	if(ret < 0) 
	{
    
    
		printk("gpio_request error!\r\n");
		return -1;
	}

	ret = gpio_direction_output(led_gpio,1);   //定义gpio为输出,默认值为1
	if(ret < 0)
	{
    
    
		printk("gpio_direction_output error!\r\n");
		return -1;
	}
	
	/*注册字符设备*/
	if (major)
    {
    
    		
		devid = MKDEV(major, 0);
		register_chrdev_region(devid,1,NODE_NAME);
	} 
    else
    {
    
    						
		alloc_chrdev_region(&devid,0,1,NODE_NAME);	//申请设备号
		major = MAJOR(devid);	//获取分配号的主设备号
		minor = MINOR(devid);
	}
	printk("gpioled major=%d,minor = %d\r\n",major,minor);	
	
	cdev.owner = THIS_MODULE;
	cdev_init(&cdev, &gpioled_fops);  //初始化字符设备
	cdev_add(&cdev,devid,1);         //添加一个字符设备

	class = class_create(THIS_MODULE,NODE_NAME);  //创建类
	if (IS_ERR(class)) 
		return PTR_ERR(class);

	device = device_create(class,NULL,devid,NULL,NODE_NAME);  //创建设备
	if (IS_ERR(device)) 
		return PTR_ERR(device);
	
	ret = platform_driver_register(&dts_device); 
	if(ret < 0) 
	{
    
    
		printk("platform_driver_register error!\n");
		return ret;
	}

	return 0;
}

static void __exit led_exit(void)
{
    
    
	gpio_free(led_gpio);
	cdev_del(&cdev);
	unregister_chrdev_region(devid,1); 
	device_destroy(class,devid);
	class_destroy(class);
	platform_driver_unregister(&dts_device);
	printk("driver exit!\n");
}

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

更规范的写法应该是下面这样。

#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>
#include <linux/platform_device.h> 

#define NODE_NAME "gpioled"

dev_t devid;			   // 设备号
struct cdev cdev;		   // 字符设备
struct class *class;	   // 类 
struct device *device;	   //  设备 
struct device_node	*nd;  // 设备节点
int major;				  // 主设备号
int minor;				  // 次设备号
int led_gpio;			 // led所使用的GPIO编号

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    
    
	int ret;
	char kbuf[1];

	ret = copy_from_user(kbuf,buf,cnt);   //将用户写入的数值传到内核
	if(ret < 0) 
    {
    
    
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}
	
	if(kbuf[0] == 1) 
    {
    
    	
		gpio_set_value(led_gpio,1);	   //给gpio写入1,打开LED
        printk("The led is turned on!\n");
	} 
    else
    {
    
    
		gpio_set_value(led_gpio,0);	  //给gpio写入0,关闭LED
        printk("The led is turned off!\n");
	}
	return 0;
}

//设备操作函数 
static struct file_operations gpioled_fops = {
    
    
	.owner = THIS_MODULE,
	.write = led_write
};

static int dts_probe(struct platform_device *pdev)
{
    
    
	int ret = 0;
	nd = of_find_node_by_path("/gpio_led");   //设备树下的节点名
	if(nd == NULL) 
    {
    
    
		printk("gpio_led node not find!\r\n");
		return -EINVAL;
	} 
    else 
		printk("gpio_led node find!\r\n");

	led_gpio = of_get_named_gpio(nd, "led-gpio", 0);
	if(led_gpio < 0) 
    {
    
    
		printk("of_get_named_gpio error!\r\n");
		return -EINVAL;
	}
	else
		printk("led-gpio num = %d\r\n",led_gpio);
	
    ret = gpio_request(led_gpio,"led");   //申请GPIO 
	if(ret < 0) 
	{
    
    
		printk("gpio_request error!\r\n");
		return -1;
	}

	ret = gpio_direction_output(led_gpio,1);   //定义gpio为输出,默认值为1
	if(ret < 0)
	{
    
    
		printk("gpio_direction_output error!\r\n");
		return -1;
	}
	
	/*注册字符设备*/
	if (major)
    {
    
    		
		devid = MKDEV(major, 0);
		register_chrdev_region(devid,1,NODE_NAME);
	} 
    else
    {
    
    						
		alloc_chrdev_region(&devid,0,1,NODE_NAME);	//申请设备号
		major = MAJOR(devid);	//获取分配号的主设备号
		minor = MINOR(devid);
	}
	printk("gpioled major=%d,minor = %d\r\n",major,minor);	
	
	cdev.owner = THIS_MODULE;
	cdev_init(&cdev, &gpioled_fops);  //初始化字符设备
	cdev_add(&cdev,devid,1);         //添加一个字符设备

	class = class_create(THIS_MODULE,NODE_NAME);  //创建类
	if (IS_ERR(class)) 
		return PTR_ERR(class);

	device = device_create(class,NULL,devid,NULL,NODE_NAME);  //创建设备
	if (IS_ERR(device)) 
		return PTR_ERR(device);
	return 0;
}

static int dts_remove(struct platform_device *pdev)
{
    
    
	gpio_free(led_gpio);
	cdev_del(&cdev);
	unregister_chrdev_region(devid,1); 
	device_destroy(class,devid);
	class_destroy(class);
	return 0;
}

const struct of_device_id of_match_table_led[] = {
    
    
	{
    
    .compatible = "gpio_uart_led"},        //与设备树中的compatible属性匹配
	//{}
};

struct platform_driver dts_device = {
    
       
	.probe = dts_probe,
	.remove = dts_remove,
	.driver = {
    
    
		.owner = THIS_MODULE,
		.name = "led",
		.of_match_table = of_match_table_led
	}
};

static int __init led_init(void)
{
    
    
	int ret = 0;
	ret = platform_driver_register(&dts_device); 
	if(ret < 0) 
	{
    
    
		printk("platform_driver_register error!\n");
		return ret;
	}
	return 0;
}

static void __exit led_exit(void)
{
    
    
	platform_driver_unregister(&dts_device);
	printk("driver exit!\n");
}

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

将原代码中进入和退出函数中的大部分工作分别移到probe函数和remove函数中,在进入函数中只对平台设备驱动进行注册,在退出函数中只对平台设备驱动注销。
Makefile文件根据自己的情况编写,下面的代码仅做参考。

obj-m := led.o
KERNEL_PATH := /home/username/linux_kernel
CUR_PATH := $(shell pwd)
all:
	make -C $(KERNEL_PATH) M=$(CUR_PATH) modules
clean:
	make -C $(KERNEL_PATH) M=$(CUR_PATH) clean

测试文件代码如下。

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

int main(int argc, char *argv[])
{
    
    
    int fd;
    char buf[1];
    fd = open("/dev/gpioled", O_RDWR);
    if(fd < 0)
    {
    
    
        perror("open error!\n"); //相当于printf("open error!\n");
        return fd; 
    }
    
/*     buf[0] = atoi(argv[1]); //字符串转换为整型
    write(fd, buf, sizeof(buf)); */
    /*如果注释掉上面两行代码,用while这段代码的话,app运行之后,led灯就会循环亮灭,每隔一秒切换一下状态*/
    while(1)
    {
    
    
        buf[0] = 1;
        write(fd, buf, sizeof(buf));
        sleep(1);
        buf[0] = 0;
        write(fd, buf, sizeof(buf));
        sleep(1);
    }
    close(fd);  
    return 0;
}

该实验也可以通过注册杂项设备实现,代码在文章Linux下点亮开发板上通过uart外接的led灯中,有兴趣的可以看一下。
将上面的代码分别编译出驱动程序和测试程序发送到开发板上进行验证。

执行结果

如果编译的时候提示命令arm-poky-linux-gnueabi-gcc找不到,就在Linux内核的Makefile文件中补全交叉编译器的路径。

/opt/fsl-imx-x11/4.1.15-2.1.0/sysroots/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-

注意,每个人的交叉编译器可能不一样,自己视情况而定,交叉编译器在Makefile文件中的位置如下图所示。
在这里插入图片描述
先加载驱动,然后通过执行测试程序向设备写数据,通过写入的数据控制LED的亮灭,开发板上打印的信息如下图所示。
在这里插入图片描述
驱动加载成功以后,在/sys/bus/platform/drivers/目录下就会多出来一个名为led的文件,如下图所示。
在这里插入图片描述
LED的状态按照测试程序中写的每隔1秒钟变化一次,执行过程中的动图如下图所示。
请添加图片描述
通过上面的结果可以看出来,LED灯驱动程序编写成功了!

在LED驱动代码中加入内核定时器

关于内核定时器的具体介绍可以参考文章Linux下内核定时器。上面的LED灯循环亮灭是通过测试程序由用户端写入的,在驱动程序中加入内核定时器以后,就不需要用户端测试程序了,因此驱动程序中也不用write函数了,直接在内核定时器到时以后向指定的gpio口写值即可,每隔指定的周期翻转一次写入的值,这样LED灯也可以实现循环亮灭。
在LED驱动代码中加入内核定时器的完整代码如下。

#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>
#include <linux/platform_device.h> 
#include <linux/timer.h>

#define NODE_NAME "gpioled"

dev_t devid;			   // 设备号
struct cdev cdev;		   // 字符设备
struct class *class;	   // 类 
struct device *device;	   //  设备 
struct device_node	*nd;  // 设备节点
int major;				  // 主设备号
int minor;				  // 次设备号
int led_gpio;			 // led所使用的GPIO编号

static int led_value = 1;
static void timer_function(unsigned long data);
DEFINE_TIMER(led_timer,timer_function,0,0);     //静态定义结构体变量并且初始化function,expires,data成员

static void timer_function(unsigned long data)
{
    
    
	mod_timer(&led_timer,jiffies + 1*HZ);  //间隔时间为1秒
    led_value = !led_value;   //翻转写入的值 
    gpio_set_value(led_gpio,led_value);   //给指定的gpio口设置数值
}

//设备操作函数 
static struct file_operations gpioled_fops = {
    
    
	.owner = THIS_MODULE
};

const struct of_device_id of_match_table_led[] = {
    
    
	{
    
    .compatible = "gpio_uart_led"},        //与设备树中的compatible属性匹配
	{
    
    }
};

struct platform_driver dts_device = {
    
        
	.driver = {
    
    
		.owner = THIS_MODULE,
		.name = "led",
		.of_match_table = of_match_table_led
	}
};

static int __init led_init(void)
{
    
    
    int ret=0;
    led_timer.expires = jiffies + 1*HZ;   //定义好未来时刻的时间点
	add_timer(&led_timer);    //向Linux内核注册定时器
    
	nd = of_find_node_by_path("/gpio_led");   //设备树下的节点名
	if(nd == NULL) 
    {
    
    
		printk("gpio_led node not find!\r\n");
		return -EINVAL;
	} 
    else 
		printk("gpio_led node find!\r\n");

	led_gpio = of_get_named_gpio(nd, "led-gpio", 0);
	if(led_gpio < 0) 
    {
    
    
		printk("of_get_named_gpio error!\r\n");
		return -EINVAL;
	}
	else
		printk("led-gpio num = %d\r\n",led_gpio);
	
    ret = gpio_request(led_gpio,"led");   //申请GPIO 
	if(ret < 0) 
	{
    
    
		printk("gpio_request error!\r\n");
		return -1;
	}

	ret = gpio_direction_output(led_gpio,1);   //定义gpio为输出,默认值为1
	if(ret < 0)
	{
    
    
		printk("gpio_direction_output error!\r\n");
		return -1;
	}
	
	/*注册字符设备*/
	if (major)
    {
    
    		
		devid = MKDEV(major, 0);
		register_chrdev_region(devid,1,NODE_NAME);
	} 
    else
    {
    
    						
		alloc_chrdev_region(&devid,0,1,NODE_NAME);	//申请设备号
		major = MAJOR(devid);	//获取分配号的主设备号
		minor = MINOR(devid);
	}
	printk("gpioled major=%d,minor = %d\r\n",major,minor);	
	
	cdev.owner = THIS_MODULE;
	cdev_init(&cdev, &gpioled_fops);  //初始化字符设备
	cdev_add(&cdev,devid,1);         //添加一个字符设备

	class = class_create(THIS_MODULE,NODE_NAME);  //创建类
	if (IS_ERR(class)) 
		return PTR_ERR(class);

	device = device_create(class,NULL,devid,NULL,NODE_NAME);  //创建设备
	if (IS_ERR(device)) 
		return PTR_ERR(device);
	
	ret = platform_driver_register(&dts_device); 
	if(ret < 0) 
	{
    
    
		printk("platform_driver_register error!\n");
		return ret;
	}

	return 0;
}

static void __exit led_exit(void)
{
    
    
	gpio_free(led_gpio);
	cdev_del(&cdev);
	unregister_chrdev_region(devid,1); 
	device_destroy(class,devid);
	class_destroy(class);
	platform_driver_unregister(&dts_device);
    del_timer(&led_timer);  //删除定时器
	printk("driver exit!\n");
}

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

将代码编译成驱动模块后发送到开发板,加载程序之后,执行结果和上面一致。


本文参考文档:
I.MX6U嵌入式Linux驱动开发指南V1.5——正点原子

猜你喜欢

转载自blog.csdn.net/weixin_42570192/article/details/133065065