66.字符设备驱动

上一节写了一个最简单的驱动,了解了驱动编写,编译及应用的方法,那么我们linux中真正的驱动是什么样的?有哪几种驱动?

 

一. linux驱动分类

字符设备 : 以字节/字符为单位(字符流)读写的设备,数据量小,不能随机读取数据。(LCD,鼠标,LED,按键,串口等)应用层通过文件IO进行操作

 

块设备   : 以块为单位读写的设备。(硬盘 flash SD卡)

 

网络设备 : 用于网络通讯设备。(socket 设备)

 

 

我还是更喜欢看下面的这个图,思路上更清晰。

 

二.linux中上层应用访问底层驱动(代码角度)

第二部分引用博主的文章,感觉讲解的很深入。

https://blog.csdn.net/kai_zone/article/details/80459334

 

在linux中一切皆文件,所有的硬件设备操作到应用层都会被抽象成文件的操作。我们知道如果应用层要访问硬件设备,它必定要调用到硬件对应的驱动程序。Linux内核有那么多驱动程序,应用怎么才能精确的调用到底层的驱动程序呢?

在这里我们拿字符设备为例,来看一下应用程序如何和底层驱动程序关联起来。

 必须知道的知识:

(1) 在Linux文件系统中,每个文件都用一个struct inode结构体来描述,这个结构体记录了这个文件的所有信息,例如文件类型,访问权限等。

(2) 在linux操作系统中,每个驱动程序在应用层的/dev目录或者其他如/sys目录下都会有一个文件与之对应。

(3) 在linux操作系统中,每打开一次文件,Linux操作系统会在VFS层分配一个struct file结构体来描述打开的文件。

(4) 在linux操作系统中,   每个驱动程序都有一个设备号。(用在众多到设备驱动中进行区分

(5) 用户必须知道设备驱动对应到设备节点(设备文件)

                     linux把所有到设备都看成文件

 

                     crw-r----- 1 root root 13, 64 Mar 28 20:14 event0

                     crw-r----- 1 root root 13, 65 Mar 28 20:14 event1

                     crw-r----- 1 root root 13, 66 Mar 28 20:14 event2

 

(6) 对设备操作其实就是对文件操作,应用空间操作open,read,write的时候

                     实际在驱动代码有对应到open, read,write

 

注意:常常我们认为,struct inode描述的是文件的静态信息,即这些信息很少会改变,而struct file描述的是动态信息,即对文件的操作的时候,struct file里面的信息经常会发生变化。典型的是struct file结构体里面的f_ops(记录当前文件的位移量),每次读写一个普通文件时f_ops的值都会发生改变。

 

 

 通过上图我们可以知道,如果想访问底层设备,就必须打开对应的设备文件。也就是在这个打开的过程中,Linux内核将应用层和对应的驱动程序关联起来。

(1) 当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体。

(2) 根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备都有一个struct cdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口。

(3) 找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中的记录的函数操作接口地址记录在struct file结构体的f_ops成员中。

(4) 任务完成,VFS层会给应用返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层应用程序就可以通过fd找到struct file,然后在有struct file找到操作字符设备的函数接口了。

 

三.如何编写字符设备驱动

下面我们以一个LED灯的驱动为实例进行讲解:

 

 

四. 字符驱动相关函数

4.1.申请设备号资源

int register_chrdev(unsigned int major, const char * name, const struct file_operations * fops)

       参数1:主设备号

                     设备号(32bit--dev_t)==主设备号(12bit) + 次设备号(20bit)

                            主设备号:表示一类设备--camera

                            次设备号: 表示一类设备中某一个:前置,后置

                     给定到方式有两种:

                            1,动态--参数1直接填0

                            2,静态--指定一个整数,250

 

       参数2: 描述一个设备信息,可以自定义

                     /proc/devices列举出所有到已经注册的设备

       参数3: 文件操作对象--提供open, read,write

       返回值: 正确返回0,错误返回负数

 

void unregister_chrdev(unsigned int major, const char * name)

       参数1:主设备号

       参数2: 描述一个设备信息,可以自定义

 

4.2.创建设备节点

1,手动创建--缺点/dev/目录中文件都是在内存中,断电后/dev/文件就会消失

               mknod /dev/设备名  类型  主设备号 次设备号

              比如:

                     mknod  /dev/chr0  c  250 0

 

              [root@farsight drv_module]# ls /dev/chr0 -l

              crw-r--r--    1 0        0         250,   0 Jan  1 00:33 /dev/chr0

 

2,自动创建(通过udev/mdev机制)

              struct class *class_create(owner, name)//创建一个类

                     参数1: THIS_MODULE

                     参数2: 字符串名字,自定义

                     返回一个class指针

 

              //创建一个设备文件

函数

struct device *device_create(struct class * class, struct device * parent, dev_t devt,void * drvdata, const char * fmt,...)

参数

参数1: class结构体,class_create调用之后到返回值

参数2:表示父亲,一般直接填NULL

参数3: 设备号类型 dev_t

              dev_t devt

              #define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))

              #define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))

              #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

参数4:私有数据,一般直接填NULL

参数5和6:表示可变参数,字符串,表示设备节点名字

              销毁动作:

                     void device_destroy(devcls,  MKDEV(dev_major, 0));

                     参数1: class结构体,class_create调用之后到返回值

                     参数2: 设备号类型 dev_t

 

                     void class_destroy(devcls);

                     参数1: class结构体,class_create调用之后到返回值

五.字符设备框架搭建

先进行一个设备号申请,及创建设备节点的实验。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>

// 静态的指定
static unsigned int dev_major = 250;
static struct class *devcls;
static struct device *dev;

ssize_t drv_read(struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
	printk("-------%s-------\n", __FUNCTION__);
	return 0;
}
ssize_t drv_write(struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
	printk("-------%s-------\n", __FUNCTION__);
	return 0;
}
int drv_open(struct inode *inode, struct file *filp)
{
	printk("-------%s-------\n", __FUNCTION__);
	return 0;
}

int drv_close(struct inode *inode, struct file *filp)
{
	printk("-------%s-------\n", __FUNCTION__);
	return 0;

}

const struct file_operations my_fops = {
	.open = drv_open,
	.read = drv_read,
	.write = drv_write,
	.release = drv_close,
};

static int __init dev_init(void)
{
	int ret;
	//申请设备号
	ret = register_chrdev(dev_major, "chr_dev_test", &my_fops);
	if(ret == 0){
		printk("register ok\n");
	}else{
		printk("register failed\n");
		return -EINVAL;
	}
	//创建一个类
	devcls = class_create(THIS_MODULE, "chr_cls");

	// 创建设备  /dev/chr2
	dev = device_create(devcls, NULL, MKDEV(dev_major, 0), NULL, "chr2");

	return 0;
}

static void __exit dev_exit(void)
{
	//释放资源
	device_destroy(devcls,  MKDEV(dev_major, 0));
	class_destroy(devcls);
	unregister_chrdev(dev_major, "chr_dev_test");

}

module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL");

Makefile(注意路径要根据自己的路径更改)

ROOTFS_DIR = /nfs/rootfs
ifeq ($(KERNELRELEASE), )

KERNEL_DIR = /home/linux/mytest/linux-3.14
CUR_DIR = $(shell pwd)
 
all :
	make -C  $(KERNEL_DIR) M=$(CUR_DIR) modules
 
clean :
	make -C  $(KERNEL_DIR) M=$(CUR_DIR) clean
	
install:
	cp -raf *.ko   $(ROOTFS_DIR)/drv_module
 
 
else

obj-m += char_dev.o

endif

 

板卡上电,通过串口打印工具,进入根文件系统下drv_module目录下,

insmod chr_drv.ko 加载驱动。

cat /proc/devices 查看一下设备,就发现已经有相应设备了。

ls /dev/chr2 查看下设备节点。

 

驱动程序有了,我们肯定要编写个应用程序进行调用一下测试测试啊,那么应用程序该怎么编写。

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

int main(int argc, char *argv[])
{
	//调用驱动
	int fd;
	int value = 0;

	fd = open("/dev/chr2", O_RDWR);
	if(fd < 0)
	{
		perror("open");
		exit(1);
	}

	read(fd, &value, 4);
	write(fd, &value, 4);
	close(fd);

	return 0;

}

改下makefile文件,一块编译驱动文件

ROOTFS_DIR = /nfs/rootfs
APP_NAME = chr_test

CROSS_COMPILE = /home/linux/Desktop/gcc-4.6.4/bin/arm-none-linux-gnueabi-
CC = $(CROSS_COMPILE)gcc

ifeq ($(KERNELRELEASE), )

KERNEL_DIR = /home/linux/mytest/linux-3.14
CUR_DIR = $(shell pwd)
 
all :
	make -C  $(KERNEL_DIR) M=$(CUR_DIR) modules
	$(CC) $(APP_NAME).c  -o $(APP_NAME)
clean :
	make -C  $(KERNEL_DIR) M=$(CUR_DIR) clean
	
install:
	cp -raf *.ko   $(ROOTFS_DIR)/drv_module
  
else

obj-m += char_dev.o

endif

在板卡上执行应用程序,查看板卡打印信息。

基本的框架已经调试通了,需要完善一下代码,实现led的控制。

 

六.LED驱动编写

1. 应用程序需要传递数据给驱动

函数

 int copy_to_user(void __user * to, const void * from, unsigned long n)

(将数据从内核空间拷贝到用户空间,一般是在驱动中drv_read()用)

参数    参数1:应用驱动中的一个buffer
    参数2:内核空间到一个buffer
    参数3:个数
返回值大于0,表示出错,剩下多少个没有拷贝成功
            等于0,表示正确

 

函数

int copy_from_user(void * to, const void __user * from, unsigned long n)

(将数据从用户空间拷贝到内核空间,一般是在驱动中drv_write()用)

参数    参数1:内核驱动中的一个buffer
    参数2:应用空间到一个buffer
    参数3:个数
返回值大于0,表示出错,剩下多少个没有拷贝成功
            等于0,表示正确

2, 控制外设,其实就是控制地址,内核驱动中是通过虚拟地址操作
    void *ioremap(cookie, size)
    参数1: 物理地址
    参数2: 长度
    返回值: 虚拟地址

    去映射--解除映射
        void iounmap(void __iomem *addr)
        参数1: 映射之后到虚拟地址

3, 通过驱动控制led灯:
        led--- GPX2_7 --- GPX2CON ==0x11000C40
                                     GPX2DAT ==0x11000C44

        将0x11000C40映射成虚拟地址
        对虚拟地址中到[32:28] = 0x1

4, 应用程序和驱动扮演的是什么角色

    用户态:应用程序
            玩策略: 怎么去做
                    1, 一闪一闪
                    2,10s闪一次,也可以1s闪一次
                    3,一直亮
                    4,跑马灯
            控制权是在应用程序(程序员)
    --------------------------------------
    内核态:驱动
            玩机制: 能做什么 
                    led:亮 和 灭


5,编写字符设备驱动到步骤和规范
    步骤:
        1,实现模块加载和卸载入口函数
                module_init(chr_dev_init);
                module_exit(chr_dev_exit);
        
        2,在模块加载入口函数中
            a, 申请主设备号  (内核中用于区分和管理不同字符设备)
                     register_chrdev(dev_major, "chr_dev_test", &my_fops);

            b,创建设备节点文件 (为用户提供一个可操作到文件接口--open())
                    struct  class *class_create(THIS_MODULE, "chr_cls");
                    struct  device *device_create(devcls, NULL, MKDEV(dev_major, 0), NULL, "chr2");

            c, 硬件的初始化
                   1,地址的映射
                        gpx2conf = ioremap(GPX2_CON, GPX2_SIZE);
                   2,中断到申请
                   3,实现硬件的寄存器到初始化
                        // 需要配置gpio功能为输出
                        *gpx2conf &= ~(0xf<<28);
                        *gpx2conf |= (0x1<<28);
            e,实现file_operations
                    const struct file_operations my_fops = {
                            .open = chr_drv_open,
                            .read = chr_drv_read,
                            .write = chr_drv_write,
                            .release = chr_drv_close,
                    };


    规范:
        1,面向对象编程思想
            用一个结构体来表示一个对象

            //设计一个类型,描述一个设备的信息
            struct led_desc{
                unsigned int dev_major; //设备号
                struct class *cls;
                struct device *dev; //创建设备文件
                void *reg_virt_base;
            };

            struct led_desc *led_dev;//表示一个全局的设备对象

            
            // 0, 实例化全局的设备对象--分配空间
            //  GFP_KERNEL 如果当前内存不够用到时候,该函数会一直阻塞(休眠)
            //  #include <linux/slab.h>
            led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);
            if(led_dev == NULL)
            {
                printk(KERN_ERR "malloc error\n");
                return -ENOMEM;
            }

            led_dev->dev_major = 250;
    
        2,做出错处理
                在某个位置出错了,要将之前申请到资源进行释放
        
            led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);    
        
            led_dev->dev_major = register_chrdev(0, "led_dev_test", &my_fops);
            if(led_dev->dev_major < 0)
            {
                printk(KERN_ERR "register_chrdev error\n");
                ret = -ENODEV;
                goto err_0;
            }


            err_0:
                kfree(led_dev);
                return ret;


6, 操作寄存器地址到方式:
    1, volatile unsigned long *gpxcon;
        *gpxcon &= ~(0xf<<28);
    
    2, readl/writel();
        u32 readl(const volatile void __iomem *addr)//从地址中读取地址空间到值

        void writel(unsigned long value , const volatile void __iomem *add)
            // 将value的值写入到addr地址
    
        例子:
            // gpio的输出功能的配置
            u32 value = readl(led_dev->reg_virt_base);
            value &= ~(0xf<<28);
            value |= (0x1<<28)
            writel(value, led_dev->reg_virt_bas);    

        或者:
                *gpx2dat |= (1<<7);
            替换成:
                writel( readl(led_dev->reg_virt_base + 4) | (1<<7),   led_dev->reg_virt_base + 4 );

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>

#include <asm/uaccess.h>
#include <asm/io.h>

//设计一个类型,描述一个设备的信息
struct led_desc{
	unsigned int dev_major; //设备号
	struct class *cls;
	struct device *dev; //创建设备文件
	void *reg_virt_base; //表示是寄存器地址到基准值
};


//物理地址
#define GPX2_CON 0x11000C40  
#define GPX2_SIZE 8

struct led_desc *led_dev;//表示一个全局的设备对象

static int kernel_val = 555;


//  read(fd, buf, size);
ssize_t led_drv_read(struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
	printk("-------%s-------\n", __FUNCTION__);

	int ret;

	ret = copy_to_user(buf,  &kernel_val, count);
	if(ret > 0)
	{
		printk("copy_to_user error\n");
		return -EFAULT;
	}
	
	return 0;

}
ssize_t led_drv_write(struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
	//printk("-------%s-------\n", __FUNCTION__);
	int ret;
	int value;
	
	ret = copy_from_user(&value,  buf, count);
	if(ret > 0)
	{
		printk("copy_to_user error\n");
		return -EFAULT;
	}

	if(value){
		writel( readl(led_dev->reg_virt_base + 4) | (1<<7),   led_dev->reg_virt_base + 4 );
		
	}else{
		writel( readl(led_dev->reg_virt_base + 4) & ~(1<<7),   led_dev->reg_virt_base + 4 );
	}
	
	return 0;
	
}
int led_drv_open(struct inode *inode, struct file *filp)
{
	printk("-------%s-------\n", __FUNCTION__);
	
	return 0;
}


int led_drv_close(struct inode *inode, struct file *filp)
{
	printk("-------%s-------\n", __FUNCTION__);
	return 0;

}

const struct file_operations my_fops = {
	.open = led_drv_open,
	.read = led_drv_read,
	.write = led_drv_write,
	.release = led_drv_close,
};

static int __init led_dev_init(void)
{
	int ret;

	// 0, 实例化全局的设备对象--分配空间
	led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);
	if(led_dev == NULL)
	{
		printk(KERN_ERR "malloc error\n");
		return -ENOMEM;
	}


	// 1, 一般都是申请设备号资源
	// 申请设备号
	led_dev->dev_major = register_chrdev(0, "led_dev_test", &my_fops);
	if(led_dev->dev_major < 0)
	{
		printk(KERN_ERR "register_chrdev error\n");
		ret = -ENODEV;
		goto err_0;
	}

	// 2,创建设备文件
	led_dev->cls = class_create(THIS_MODULE, "led_cls");
	if(IS_ERR(led_dev->cls))
	{
		printk(KERN_ERR "class_create error\n");
		ret = PTR_ERR(led_dev->cls); //将指针出错的具体原因转换成一个出错码
		goto err_1;
	}

	// /dev/led0
	led_dev->dev = device_create(led_dev->cls, NULL, 
				MKDEV(led_dev->dev_major, 0), NULL, "led%d", 0);
	if(IS_ERR(led_dev->dev))
	{
		printk(KERN_ERR "device_create error\n");
		ret = PTR_ERR(led_dev->dev); //将指针出错的具体原因转换成一个出错码
		goto err_2;
	}

	// 3,硬件初始化
	// 对地址进行映射
	led_dev->reg_virt_base = ioremap(GPX2_CON, GPX2_SIZE);
	if(led_dev->reg_virt_base == NULL)
	{
		
		printk(KERN_ERR "ioremap error\n");
		ret = -ENOMEM;
		goto err_3;
	}

	// gpio的输出功能的配置
	u32 value = readl(led_dev->reg_virt_base);
	value &= ~(0xf<<28);
	value |= (0x1<<28);
	writel(value, led_dev->reg_virt_base);	
	
	return 0;
	

err_3:
	device_destroy(led_dev->cls, MKDEV(led_dev->dev_major, 0));

err_2:
	class_destroy(led_dev->cls);

err_1:
	unregister_chrdev(led_dev->dev_major, "led_dev_test");

err_0:
	kfree(led_dev);
	return ret;

}

static void __exit led_dev_exit(void)
{
	//一般都是释放资源
	iounmap(led_dev->reg_virt_base);
	device_destroy(led_dev->cls, MKDEV(led_dev->dev_major, 0));
	class_destroy(led_dev->cls);
	unregister_chrdev(led_dev->dev_major, "led_dev_test");
	kfree(led_dev);

}

module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");

应用层

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>


int main(int argc, char *argv[])
{
	//调用驱动

	int fd;
	int value = 0;

	fd = open("/dev/led0", O_RDWR);
	if(fd < 0)
	{
		perror("open");
		exit(1);
	}

	read(fd, &value, 4);

	printf("___USER___:  value = %d\n", value);


	//应用程序去控制灯到亮和灭

	while(1)
	{
		value = 0;
		write(fd, &value, 4);
		sleep(1);

		value = 1;
		write(fd, &value, 4);
		sleep(1);		
	}

	close(fd);
	return 0;

}

Makefile

ROOTFS_DIR = /opt/4412/rootfs

MODULE_NAME = led_drv
APP_NAME = led_test

CROSS_COMPILE = /home/george/Linux_4412/toolchain/gcc-4.6.4/bin/arm-none-linux-gnueabi-
CC = $(CROSS_COMPILE)gcc

ifeq ($(KERNELRELEASE), )

KERNEL_DIR = /home/george/Linux_4412/kernel/linux-3.14
CUR_DIR = $(shell pwd)

all :
	make -C  $(KERNEL_DIR) M=$(CUR_DIR) modules
	$(CC) $(APP_NAME).c  -o $(APP_NAME)

clean :
	make -C  $(KERNEL_DIR) M=$(CUR_DIR) clean
	rm -rf $(APP_NAME)	

install:
	cp -raf *.ko $(APP_NAME)   $(ROOTFS_DIR)/drv_module

else

obj-m += $(MODULE_NAME).o


endif

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
对于驱动数码管(7段显示器)的MSP430F5529微控制器,你可以使用以下步骤进行驱动: 1. 配置引脚:选择用于连接数码管的引脚,并将其配置为输出模式。可以使用MSP430F5529的GPIO模块来完成此操作。 2. 定义数码管编码:根据所使用的数码管类型(共阳极或共阴极),定义相应的数码管编码。每个数字和字母都有对应的编码,用于在数码管上显示相应的字符。 3. 编写显示函数:编写一个函数,该函数接受要在数码管上显示的字符或数字,并根据数码管编码设置相应的引脚状态,以在数码管上显示正确的字符。 4. 主循环中调用显示函数:在主循环中调用显示函数,以便持续更新数码管上显示的内容。 下面是一个简单的示例代码,演示如何驱动共阳极的4位7段数码管: ```c #include <msp430.h> // 数码管编码 const unsigned char digitCode[] = { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F // 9 }; // 配置引脚 void setupPins() { // 设置数码管引脚为输出模式 P1DIR |= BIT0 | BIT1 | BIT2 | BIT3; // P1.0 - P1.3 P2DIR |= BIT4 | BIT5 | BIT6 | BIT7; // P2.4 - P2.7 } // 显示函数 void displayNumber(unsigned int number) { // 获取个位数码管编码并显示 unsigned char digit = number % 10; P1OUT = digitCode[digit]; // 去掉个位数 number /= 10; // 获取十位数码管编码并显示 digit = number % 10; P2OUT = digitCode[digit]; } int main(void) { WDTCTL = WDTPW | WDTHOLD; // 停用看门狗定时器 setupPins(); // 配置引脚 while(1) { displayNumber(42); // 在数码管上显示数字42 } return 0; } ``` 请注意,该示例代码仅为演示目的,实际应用中可能需要根据具体情况进行修改。另外,如果使用的是共阴极的数码管,可以根据需要修改数码管编码或更改引脚状态。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值