led驱动开发(设备树)

一、创建工程

1、知识前提

​ 一般编写驱动很少直接操作寄存器来初始化外设,但是不排除要调试时要读取或置位寄存器的某些位。
​ 在linux系统下操作寄存器不能直接按照芯片手册的寄存器物理地址来操作,因为linux内核启动的时候会初始化MMU,MMU的作用是将物理内存和虚拟内存映射起来,也就是地址映射:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xo1k6ggb-1584096046047)(C:\Users\10309\AppData\Roaming\Typora\typora-user-images\image-20200303124904617.png)]

​ 对于32位处理器来说,虚拟地址范围是232 = 4GB,而开发板上的物理内存不一定达到4GB,因此物理地址和虚拟地址不是逐一对应的,不同的虚拟地址可能映射到同一个物理地址中。而linux内核启动后CPU访问的都是虚拟地址,因此直接访问寄存器的物理地址是操作不了寄存器的。必须得到寄存器物理地址对应的虚拟地址,使用 ioremapiounmap 两个函数可以确定虚拟地址和物理地址之间关系。

ioremap函数

​ ioremap函数用于获取指定物理地址空间对应的虚拟地址空间,定义在linux内核源码arch/arm/include/asm/io.h文件中,调用前要添加头文件。函数定义如下:

#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE) //函数宏定义
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype) //函数定义
{
    return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0));
}

​ ioremap是个宏,有两个参数:
​ cookie:要映射的物理起始地址
​ size:要映射的内存空间大小(单位:字节),可对指定内存块进行操作。
​ 返回值:__iomem类型的指针,指向映射后的虚拟空间首地址

iounmap函数

​ 卸载驱动时需要使用iounmap函数来释放ioremap函数所做的映射。函数原型如下:

void iounmap (volatile void __iomem *addr)

​ addr:取消映射的虚拟地址空间首地址。

io内存访问函数

​ linux内核不建议直接通过地址指针来访问内存,提供一些读写函数来访问内存。
读操作函数如下:

u8 readb(const volatile void __iomem *addr)		//读8位数据
u16 readw(const volatile void __iomem *addr) 	//读16位数据
u32 readl(const volatile void __iomem *addr)	//读32位数据

写操作函数如下:

void writeb(u8 value, volatile void __iomem *addr)		//写8位数据
void writew(u16 value, volatile void __iomem *addr)		//写16位数据
void writel(u32 value, volatile void __iomem *addr)		//写32位数据

2、添加头文件路径

​ 因为是编写 linux 驱动,因此会用到 linux 源码中的函数。我们需要在 VSCode 中添加 linux 源码中的头文件路径。按下 “Ctrl + Shift + P” 打开控制台,然后输入 “C/C++: Edit configurations(JSON)”,打开C/C++编辑配置文件,打开以后会自动在 .vscode 文件下生成一个名为 c_cpp_properties.json 的文件,然后添加 linux 源码的头文件路径如下:

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/liuzhikai/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/include",
                "/home/liuzhikai/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include",
                "/home/liuzhikai/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated/"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "clang-x64"
        }
    ],
    "version": 4
}

3、编写加载和卸载注册函数

​ linux 驱动有两种运行方式,一是编译进 linux 内核中,内核启动时自动运行驱动程序;二是将驱动编译成模块(模块扩展名为 xx.ko),然后内核启动后才用命令加载驱动模块。模块有加载卸载两种操作,加载和卸载注册函数如下:

module_init(dtsled_init);   //注册模块加载函数
module_exit(dtsled_exit);   //注册模块卸载函数

​ 参数 dtsled_init 和 dtsled_exit 是需要注册的具体函数,加载驱动的时候就会调用 led_init ,卸载驱动的时候会调用 led_exit 函数。两个函数的模板如下:

入口函数:

#define DEVICE_NAME "led"
#define LED_MAJOR 200

/* 驱动入口函数,加载驱动时调用 */
static int __init dtsled_init(void)
{
	int ret = 0;	//返回值
	u32 register_data = 0;	
	struct property *compatible = NULL;		//compatible属性
	struct device_node *nd;		//设备节点
	const char *status;		//status属性
	u32 regdata[10];	//reg属性数组
	
	/* 获取设备节点 */
	nd = of_find_node_by_name(NULL, "led_dts");
	if(nd == NULL){		//获取节点失败
		ret = -ENODEV;
		goto fail_find_nd;
	}

	/* 获取compatible属性 */
	compatible = of_find_property(nd, "compatible", NULL);
	if(compatible == NULL){		//获取compatible属性失败
		ret = -ENODEV;
		goto fail_find_compatible;
	}else{	//获取到compatible属性
		printk("The value of compatible is %s\r\n", (char*)compatible->value);
	}

	/* 获取status属性 */
	ret = of_property_read_string(nd, "status", &status);
	if(ret < 0){	//读取属性失败
		ret = -ENODEV;
		goto fail_find_status;
	}else{	//获取到属性值
		printk("The value of status is %s\r\n", status);
	}

	/* 获取reg属性的值 */
	ret = of_property_read_u32_array(nd, "reg", regdata, 10);
	if(ret != 0){	//读取失败
		ret = -ENODEV;
		goto fail_get_reg;
	}else{	//读取成功
		u8 i;
		printk("reg data:\r\n");
		for(i = 0; i < 10; i++)
			printk("regdata[%d]:%#x\r\n", i, regdata[i]);
	}

	/* 初始化led */
	/* 获取映射虚拟地址 */
	CCM_CCGR1 = ioremap(regdata[0], regdata[1]);
	SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);
	SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);
	GPIO1_GDIR = ioremap(regdata[6], regdata[7]);
	GPIO1_DR = ioremap(regdata[8], regdata[9]);

	/* 设置IO复用 */
	writel(0x5, SW_MUX_GPIO1_IO03);

	/* 设置电气属性 */
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 初始化GPIO */
	register_data = readl(GPIO1_GDIR);
	register_data |= (1 << 3);
	writel(register_data, GPIO1_GDIR);
	led_switch(LED_ON);

	/* 处理设备号 */
	dtsled.major = 0;
	if(dtsled.major){	//如果设置了主设备号
		dtsled.devid = MKDEV(dtsled.major, 0);	//创建设备号
		register_chrdev_region(dtsled.devid, DEVICE_COUNT, DEVICE_NAME);  //注册设备号
	}else{	//如果没有设置主设备号
		ret = alloc_chrdev_region(&dtsled.devid, 0, DEVICE_COUNT, DEVICE_NAME);  //申请设备号
		if(ret < 0){	//申请设备号错误
			printk("fail to alloc devid\r\n");
			goto fail_devid;
		}
		dtsled.major = MAJOR(dtsled.devid);  
		dtsled.minor = MINOR(dtsled.devid);
		printk("The major is %d\r\nThe minor is %d\r\n", dtsled.major, dtsled.minor);
	}

	/* 注册设备 */
	dtsled.cdev.owner = THIS_MODULE;	//初始化owner
	cdev_init(&dtsled.cdev, &dtsled_fops);		//初始化cdev
	ret = cdev_add(&dtsled.cdev, dtsled.devid, 1);	//注册设备
	if(ret){	//注册失败
		printk("Error %d adding DEMO\r\n", ret);
		goto fail_cdev;
	}

	/* 自动创建节点 */
	dtsled.class = NULL;
	dtsled.device = NULL;
	dtsled.class = class_create(THIS_MODULE, DEVICE_NAME);	//创建类
	if(dtsled.class == NULL){	//创建类失败
		ret = -ENODEV;
		printk("Error %d creating class\r\n", ret);
		goto fail_create_class;
	}
	dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DEVICE_NAME);	//创建设备
	if(dtsled.device == NULL){	//创建类失败
		ret = -ENODEV;
		printk("Error %d creating device\r\n", ret);
		goto fail_create_device;
	}

	printk("dtsled init!\r\n");
	return 0;

fail_find_nd:
fail_find_compatible:
fail_find_status:
fail_get_reg:
fail_devid: 
fail_cdev: 
	unregister_chrdev_region(dtsled.devid, DEVICE_COUNT);	//注销设备号
fail_create_class:
	cdev_del(&dtsled.cdev);		//删除设备
	unregister_chrdev_region(dtsled.devid, DEVICE_COUNT);	//注销设备号
fail_create_device:
	class_destroy(dtsled.class);	//删除类
	cdev_del(&dtsled.cdev);		//删除设备
	unregister_chrdev_region(dtsled.devid, DEVICE_COUNT);	//注销设备号

	return ret;
}

出口函数:

/* 驱动出口函数,卸载驱动时调用 */
static void __exit dtsled_exit(void)
{
	led_switch(LED_OFF);

	/* 取消映射 */
	iounmap(CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_GDIR);
	iounmap(GPIO1_DR);

	/* 删除设备 */
	device_destroy(dtsled.class, dtsled.devid);
	/* 删除类 */
	class_destroy(dtsled.class);
	/* 注销字符设备 */
	unregister_chrdev_region(dtsled.devid, DEVICE_COUNT);
	/* 注销设备号 */
	unregister_chrdev_region(dtsled.devid, DEVICE_COUNT);

	printk("dtsled exit!\r\n");
}

3、编写设备的具体操作函数

​ 注册函数中 file_operations 类型的结构体定义了实际注册的操作函数,file_operations 结构体有很多成员函数,这些函数不一定全都需要注册,挑选用到的注册即可,各成员函数的模板可以参考 linux 内核的驱动。在注册函数前定义 dtsled_fops 结构体和操作函数:

/* open函数 */
static int dtsled_open(struct inode *inode, struct file *filp)
{
	return 0;
}

/* release函数 */
static int dtsled_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* write函数 */
static ssize_t dtsled_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
	int ret = 0;
	u8 led_state[1];

	ret = copy_from_user(led_state, buf, count);
	if(ret < 0){
		printk("recieve cmd wrong\r\n");
		return -1;
	}
	if(led_state[0] == LED_ON)
	{
		led_switch(LED_ON);
	}
	else{
		led_switch(LED_OFF);
	}

	return 0;
}

/* 操作函数集合 */
static const struct file_operations dtsled_fops = {
	.owner = THIS_MODULE,
	.write = dtsled_write,
	.open  = dtsled_open,
	.release = dtsled_release,
};

4、添加头文件及创建虚拟地址指针

​ 参考 linux 内核的驱动代码时,找到可能用到的头文件,添加进工程。在调用系统调用函数库函数时,在终端使用 man 命令可查看调用的函数需要包含哪些头文件。
​ man 命令数字含义:1:标准命令 2:系统调用 3:库函数
​ 添加以下头文件:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>		//两个拷贝函数所需头文件
#include <asm/io.h>			    //ioremap iounmap 函数所需头文件
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/of_platform.h>
#include <linux/slab.h>

#define DEVICE_NAME "dtsled"
#define DEVICE_COUNT 1
#define LED_ON   1
#define LED_OFF	 0

/* 寄存器映射虚拟地址 */
static void __iomem *CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_GDIR;
static void __iomem *GPIO1_DR;

5、添加 License 和作者信息

​ 驱动的 License 是必须的,缺少的话会报错,在文件最末端添加以下代码:

MODULE_LICENSE("GPL");
MODULE_AUTHOR("lzk");

6、编写led状态切换函数

void led_switch(u8 led_state)
{
	u32 register_data = 0;
	if(led_state == LED_ON){  //打开led灯
		register_data = readl(GPIO1_DR);
		register_data &= ~(1 << 3);
		writel(register_data, GPIO1_DR);
	}
	else{	//关闭led灯
		register_data = readl(GPIO1_DR);
		register_data |= (1 << 3);
		writel(register_data, GPIO1_DR);
	}
}

二、编写测试应用程序

​ 在驱动文件夹下创建 ledAPP.c 文件:

#include <sys/types.h>		// open 函数所需头文件
#include <sys/stat.h>		//open 函数所需头文件
#include <fcntl.h>			//open 函数所需头文件
#include <stdio.h>           //printf 函数所需头文件
#include <unistd.h>			//read write close 函数所需头文件
#include <string.h>			//memcpy 函数所需头文件
#include <stdlib.h>			//atoi 函数所需头文件

#define LED_ON   1
#define LED_OFF	 0
/*
应用程序使用 arm-linux-gnueabihf-gcc ledAPP.c -o ledAPP来编译,生成可执行文件
执行应用程序时使用命令行:./ledAPP /dev/ledAPP 1
命令信息保存在 main 函数的两个入口参数中
argc:为命令参数个数,为 3
argv:为命令各个参数的具体内容:
argv[0]:打开应用程序
argv[1]:open读取的文件,驱动文件都在/dev目录下
argv[2]:定义读写功能,1 为开灯 ,0为关灯
命令所带参数可自行定义
*/
int main(int argc, char *argv[])
{
    int fd = 0; //文件描述符,读取文件之前要用open函数打开文件,打开成功后得到文件描述符
    int ret = 0; //read write函数返回值
    unsigned char led_state[1]; //保存led状态
    char *filename; //open函数读取的文件,为argv[2]

    if(argc != 3) //如果命令参数不等于3,表明输入命令格式不对
    {
        printf("missing parameter!\r\n");
        return -1;
    }

    filename = argv[1]; //获取驱动文件名
    fd = open(filename , O_RDWR); //打开驱动文件,O_RDWR表明读写模式(man 2 open 查看具体)
    if(fd < 0) //如果文件描述符小于0,则表明打开文件失败
    {
        printf("open file %s failed\r\n", filename);
        return -1;
    }
    
    /*
    写操作
    命令行传递的参数均为字符,atoi函数把字符数字转化成整型
    */
    if(atoi(argv[2]) == 1){
        led_state[0] = LED_ON; //开灯
        ret = write(fd, led_state, sizeof(led_state)); //写入led状态
        if(ret < 0){
            printf("write file %s failed\r\n", filename);
            return -1;
        }
    }
    else{
        led_state[0] = LED_OFF; //关灯
        ret = write(fd, led_state, sizeof(led_state)); //写入led状态
        if(ret < 0){
            printf("write file %s failed\r\n", filename);
            return -1;
        }
    }

    ret = close(fd); //关闭文件
    if(ret < 0) //返回值小于0关闭文件失败
    {
        printf("close file %s failed\r\n", filename);
        return -1;
    }
	return 0;
}

三、编译和测试

1、编写Makefile,编译驱动程序

​ 驱动程序源码需要编译成.ko模块,创建Makefile:

KERNELDIR := /home/liuzhikai/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := dtsled.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
  • 第1行,KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径。

  • 第2行,CURRENT_PATH 表示当前路径。

  • 第3行,obj-m 表示将 led.c 这个文件编译为模块。

  • 第5行,默认目标为 kernel_modules。

  • 第8行,具体的编译命令,后面的 modules 表示编译模块,-C表示将当前的工作目录切换到指定目录中。M表示模块源码目录。“make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块源码并将其编译出 .ko文件。

    编译成功以后会生成一个 led.ko 文件,这个文件就是驱动模块。

2、编译驱动程序

​ 测试 APP 要在 ARM 开发板上运行,所以使用交叉编译器编译:

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

​ 编译完成生成 ledAPP 的可执行程序。

3、运行测试

​ linux 系统选择网络启动,并且用 nfs 挂载根文件系统,U-Boot 设置如下:
​ bootcmd 的值:

tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000

​ bootargs 的值:

console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.138:/home/liuzhikai/linux/nfs/rootfs ip=192.168.1.123:192.168.1.138:192.168.1.2:255.255.255.0::eth0:off

​ 将 dtsled.koledAPP 拷贝到以下目录(不存在的目录创建):

sudo cp dtsled.ko ledApp /home/liuzhikai/linux/nfs/rootfs/lib/modules/4.1.15/ -f

​ 使用如下命令加载驱动模块:

depmod
modprobe dtsled.ko  //加载驱动模块
./ledAPP /dev/dtsled 1  //执行应用程序,打开led灯
./ledAPP /dev/dtsled 0  //执行应用程序,关闭led灯
rmmod dtsled.ko  //卸载驱动模块

args 的值:

console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.138:/home/liuzhikai/linux/nfs/rootfs ip=192.168.1.123:192.168.1.138:192.168.1.2:255.255.255.0::eth0:off

​ 将 dtsled.koledAPP 拷贝到以下目录(不存在的目录创建):

sudo cp dtsled.ko ledApp /home/liuzhikai/linux/nfs/rootfs/lib/modules/4.1.15/ -f

​ 使用如下命令加载驱动模块:

depmod
modprobe dtsled.ko  //加载驱动模块
./ledAPP /dev/dtsled 1  //执行应用程序,打开led灯
./ledAPP /dev/dtsled 0  //执行应用程序,关闭led灯
rmmod dtsled.ko  //卸载驱动模块
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值