Linux字符设备驱动_基础

1、 字符设备驱动

1.1 字符设备驱动的框架

在这里插入图片描述

1.2 注册字符设备驱动的函数

#include <linux/fs.h>
1. register_chrdev ----> 向内核注册字符设备
	int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
	功能:向内核注册字符设备驱动
    参数:
    	@ major : 主设备号
            major = 0 :主设备号由内核自动分配(不需要考虑主设备号是否被占用)
			major > 0 : major就是主设备号(需要考虑主设备号是否被占用)
                如何查看已经使用的设备号:cat /proc/devices
        @ name : 设备文件的名字
        @ fops : 操作方法结构体的指针变量的地址
	struct file_operations {
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        int (*open) (struct inode *, struct file *);
        int (*release) (struct inode *, struct file *);
    }; 

	返回值:
        major = 0 : 成功返回分配成功之后的主设备号,失败返回错误码
        major > 0 : 成功返回0, 失败返回错误码
    
     错误码: EIO , 通过EIO错误码查看其他的错误码
            由于错误码的宏定义是一个正数,因此再返回错误码时需要再前边加一个符号,
            例子 : return -EIO;
            #define	EPERM		 1	/* Operation not permitted */
            #define	ENOENT		 2	/* No such file or directory */
            #define	ESRCH		 3	/* No such process */
            #define	EINTR		 4	/* Interrupted system call */
            #define	EIO		 5	/* I/O error */
            #define	ENXIO		 6	/* No such device or address */
            #define	E2BIG		 7	/* Argument list too long */
            #define	ENOEXEC		 8	/* Exec format error */
            #define	EBADF		 9	/* Bad file number */
            #define	ECHILD		10	/* No child processes */
            #define	EAGAIN		11	/* Try again */
            #define	ENOMEM		12	/* Out of memory */
            #define	EACCES		13	/* Permission denied */
            #define	EFAULT		14	/* Bad address */
            #define	ENOTBLK		15	/* Block device required */
            #define	EBUSY		16	/* Device or resource busy */
            #define	EEXIST		17	/* struct file_operations */
            #define	EXDEV		18	/* Cross-device link */
            #define	ENODEV		19	/* No such device */
            #define	ENOTDIR		20	/* Not a directory */
            #define	EISDIR		21	/* Is a directory */
            #define	EINVAL		22	/* Invalid argument */
            #define	ENFILE		23	/* File table overflow */
            #define	EMFILE		24	/* Too many open files */
            #define	ENOTTY		25	/* Not a typewriter */
            #define	ETXTBSY		26	/* Text file busy */
            #define	EFBIG		27	/* File too large */
            #define	ENOSPC		28	/* No space left on device */
            #define	ESPIPE		29	/* Illegal seek */
            #define	EROFS		30	/* Read-only file system */
            #define	EMLINK		31	/* Too many links */
            #define	EPIPE		32	/* Broken pipe */
            #define	EDOM		33	/* Math argument out of domain of func */
            #define	ERANGE		34	/* Math result not representable */ 
2. unregister_chrdev  ---> 注销字符设备
    void unregister_chrdev(unsigned int major, const char *name)
    功能:注销字符设备驱动
    参数:
    	@ major : 主设备号
        @ name : 字符设备的名字
    返回值:

1.3 字符设备驱动测试代码

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

#define MYNAME  "mycdev"

int major;

int mycdev_open (struct inode *inode, struct file *file)
{
    printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
    return 0;
}
ssize_t mycdev_read (struct file *file, char __user *ubuf, size_t size, loff_t *offset)
{
    printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
    return 0;
}
ssize_t mycdev_write (struct file *file, const char __user *ubuf, size_t size, loff_t *offset)
{
    printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
    return 0;
}
int mycdev_close (struct inode *inode, struct file *file)
{
    printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
    return 0;
}

struct file_operations fops = {
    .open = mycdev_open,
    .read = mycdev_read,
    .write = mycdev_write,
    .release = mycdev_close,
};
static int __init mycdev_init(void)
{
    // 注册字符设备
    major = register_chrdev(0, MYNAME, &fops);
    if (major < 0) {
        printk("register char device failed!\n");
        return -EIO;
    }
    return 0;
}

static void __exit mycdev_exit(void)
{
    unregister_chrdev(major, MYNAME);
}
module_init(mycdev_init);
module_exit(mycdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("zk <zhouk_bj@hqyj.com>");

1.4 应用层的代码实现


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

char u_buf[100] = {0};
int main(int argc, const char *argv[])
{
    /*your code*/
    int fd;
    fd = open("/dev/mycdev", O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }
    read(fd, u_buf, sizeof(u_buf));
    write(fd, u_buf, sizeof(u_buf));
    close(fd);
    return 0;
}

1.5 修改Makefile文件,编译应用层的代码


modname ?= 

arch ?= x86
ifeq ($(arch),x86)
	KERNELDIR := /lib/modules/$(shell uname -r)/build
	CROSS_COMPILE := 
else
	KERNELDIR := /home/linux/DC22041/porting/linux/linux-stm32mp-5.10.61-stm32mp-r2-r0/linux-5.10.61/
	CROSS_COMPILE := arm-linux-gnueabihf-
endif

CURRENTDIR := $(shell pwd)

CC := $(CROSS_COMPILE)gcc
all:
	make -C $(KERNELDIR) M=$(CURRENTDIR) modules
	$(CC) test.c -o test
install:
	@cp $(modname).ko  ~/nfs/rootfs/ 
	@cp test ~/nfs/rootfs

help:
	@echo "make arch=arm|x86 modname=modules drivers source file name"

clean:
	make -C $(KERNELDIR) M=$(CURRENTDIR) clean
	
obj-m := $(modname).o

1.6 加载驱动并测试驱动的步骤

1> 加载驱动 : sudo insmod mycdev.ko
2> 查看驱动对应的主设备号 : cat /proc/devices, 需要查看自己的主设备号
3> 在/dev目录下创建设备文件 sudo mknod  /dev/mycdev c  236  0 
	
	mknod  : 创建设备文件的命令
	/dev/mycdev  : 将设备文件创建到/dev目录下,默认设备文件存到/dev目录下,当然也可以放到其他目录下
	c   : 表示字符设备文件,  如果写b:表示块设备文件
	236  : 主设备号
    0  : 次设备号
4> 运行应用程序: sudo ./test
5> 使用dmesg查看驱动中的open read write close函数是否被调用,如下图所示
6> 卸载驱动  sudo rmmod  mycdev

在这里插入图片描述

1.7 自己编写驱动代码的思路

1> 先写模块化驱动的三要素:入口,出口,许可证
2> 在入口函数中调用register_chrdev函数,函数需要什么就准备对应的变量
3> 在出口函数中调用unregister_chrdev函数
4> 编译驱动的程序,如果有错误进行修改,没有错误编写应用程序进行测试。

2、 用户空间和内核空间直接数据的拷贝

2.1 用户空间和内核空间数据拷贝的API

#include <linux/uaccess.h>
1. copy_to_user 
    unsigned long copy_to_user(void __user *to, const void *from, 
                               unsigned long n)
    功能:将内核空间中的数据拷贝到用户空间
    参数:
    	@ to : 用户空间的地址
        @ from : 内核空间的地址
        @ n : 拷贝数据的大小
    返回值:
         成功返回0,失败返回未拷贝的数据的个数
2. copy_from_user 
    unsigned long copy_from_user(void *to, const void __user *from, 
                                 unsigned long n)
    功能:将用户空间中的数据拷贝到内核空间
    参数:
    	@ to : 内核空间的地址
        @ from : 用户空间的地址
        @ n : 拷贝数据的大小
    返回值:
         成功返回0,失败返回未拷贝的数据的个数

2.2 系统调用函数和驱动中的read/write对应关系

在这里插入图片描述

2.3 用户空间和内核空间数据拷贝的驱动代码实现

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

#define MYNAME  "mycdev"

int major;
char kbuf[128] = "I am kernel spcae data!";
int mycdev_open (struct inode *inode, struct file *file)
{
    printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
    return 0;
}
ssize_t mycdev_read (struct file *file, char __user *ubuf, size_t size, loff_t *offset)
{   
    int ret;
    printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
    // 校准拷贝数据的大小
    if (size > sizeof(kbuf)) size = sizeof(kbuf);
    // 将内核空间的数据拷贝到用户空间
    ret = copy_to_user(ubuf,kbuf,size);
    if (ret) {
        printk("kernel space copy data to user space failed!\n");
        return -EINVAL;
    }
    // 成功返回拷贝成功的字节的个数
    return size;
}
ssize_t mycdev_write (struct file *file, const char __user *ubuf, size_t size, loff_t *offset)
{
    int ret;
    printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
     // 校准拷贝数据的大小
    if (size > sizeof(kbuf)) size = sizeof(kbuf);
    // 将内核空间的数据拷贝到用户空间
    ret = copy_from_user(kbuf,ubuf,size);
    if (ret) {
        printk("user space copy data to kernel space failed!\n");
        return -EINVAL;
    }

    printk("kernel space = %s\n", kbuf);
    return size;
}
int mycdev_close (struct inode *inode, struct file *file)
{
    printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
    return 0;
}

struct file_operations fops = {
    .open = mycdev_open,
    .read = mycdev_read,
    .write = mycdev_write,
    .release = mycdev_close,
};
static int __init mycdev_init(void)
{
    // 注册字符设备
    major = register_chrdev(0, MYNAME, &fops);
    if (major < 0) {
        printk("register char device failed!\n");
        return -EIO;
    }
    return 0;
}

static void __exit mycdev_exit(void)
{
    unregister_chrdev(major, MYNAME);
}
module_init(mycdev_init);
module_exit(mycdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("zk <zhouk_bj@hqyj.com>");

2.4 用户空间和内核空间数据拷贝应用层代码实现


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

char u_buf[128] = {0};
int main(int argc, const char *argv[])
{
    /*your code*/
    int fd;
    fd = open("/dev/mycdev", O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }
    read(fd, u_buf, sizeof(u_buf));
    printf("user space = %s\n", u_buf);
    strcpy(u_buf, "I am user space data!");
    write(fd, u_buf, sizeof(u_buf));
    close(fd);
    return 0;
}

2.5 代码测试

1> 加载驱动 : sudo insmod mycdev.ko
2> 查看驱动对应的主设备号 : cat /proc/devices, 需要查看自己的主设备号
3> 在/dev目录下创建设备文件 sudo mknod  /dev/mycdev c  236  0 
4> 运行应用程序: 	
	linux@ubuntu:02mycdev$ sudo ./test 
	user space = I am kernel spcae data!
5> 使用dmesg查看驱动中的open read write close函数是否被调用,如下图所示
6> 卸载驱动  sudo rmmod  mycdev

在这里插入图片描述

3、led灯驱动的实现

3.1 在内核空间能否操作特殊功能寄存器地址

特殊功能寄存器的地址是物理地址,而内核空间的地址是3-4G的虚拟地址,
如果直接在内核空间操作特殊功能寄存器的物理地址空间,会导致内核出错。
如果想在内核空间操作特殊功能寄存器的物理地址,需要将物理地址映射为虚拟地址,
在内核空间操作虚拟地址最终相当于操作了特殊功能寄存器的物理地址。

mmu(memory manager unit) : 由内存管理单元完成物理地址到虚拟地址的映射。

使用ioremap函数完成物理地址到虚拟地址的映射; 使用iounmap函数完成取消物理地址到虚拟地址的映射。

3.2 物理地址到虚拟地址映射的API

#include <linux/io.h>
1. ioremap
	void  *ioremap(phys_addr_t paddr, unsigned long size)
	功能:完成物理地址到虚拟地址的映射
    参数:
    	@ paddr : 物理地址
    	@ size : 映射空间的大小,以字节为单位
	返回值:
    	成功返回虚拟的地址
    	失败返回NULL
2. iounmap
	void iounmap(const void __iomem *addr)
    功能:取消物理地址到虚拟地址的映射
    参数:
    	@ addr : 虚拟地址
    返回值:无  

3.3 完成LED灯的驱动

3.3.1 分析led1灯的电路图

在这里插入图片描述
在这里插入图片描述

3.3.2 分析芯片手册

1. RCC_MP_AHB4ENSETR[4]  ----> 0x50000A28 
	写1,使能GPIOE外设的时钟
2. GPIOx_MODER[21:20] ----> 0x50006000
	写0b01, 设置为通用的输出模式

3. GPIOx_OTYPER[10]  ----> 0x50006004
	写0b0 , 设置为推挽输出
	
4. GPIOx_OSPEEDR[21:20]  ----> 0x50006008
	写0b00, 设置为低速模式
	
5. GPIOx_PUPDR[21:20]  -----> 0x5000600C
	写0b00,禁止上拉和下拉

6. GPIOx_ODR[10] ----> 0x50006014
	写0, PE10输出低电平
	写1, PE10输出高电平

3.3.3 编写led灯的驱动代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>

#define MYNAME  "myled"
#define RCC_MP_AHB4ENSETR_PHY_ADDR  0x50000A28
#define GPIOE_MODER_PHY_ADDR    0x50006000
#define GPIOE_OTYPER_PHY_ADDR   0x50006004
#define GPIOE_OSPEEDR_PHY_ADDR  0x50006008
#define GPIOE_PUPDR_PHY_ADDR    0x5000600C
#define GPIOE_ODR_PHY_ADDR      0x50006014

volatile unsigned int * rcc_virl_addr;
volatile unsigned int * gpioe_moder_virl_addr;
volatile unsigned int * gpioe_otyper_virl_addr;
volatile unsigned int * gpioe_ospeedr_virl_addr;
volatile unsigned int * gpioe_pupdr_virl_addr;
volatile unsigned int * gpioe_odr_virl_addr;
int major;
char kbuf[128] = {0};
int myled_open (struct inode *inode, struct file *file)
{
    printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
    return 0;
}
ssize_t myled_read (struct file *file, char __user *ubuf, size_t size, loff_t *offset)
{   
    int ret;
    printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
    // 校准拷贝数据的大小
    if (size > sizeof(kbuf)) size = sizeof(kbuf);
    // 将内核空间的数据拷贝到用户空间
    ret = copy_to_user(ubuf,kbuf,size);
    if (ret) {
        printk("kernel space copy data to user space failed!\n");
        return -EINVAL;
    }
    // 成功返回拷贝成功的字节的个数
    return size;
}
/*
    需要指定一个led灯控制的协议
    kbuf[0] = 1 : 表示控制LED1灯
    kbuf[0] = 2 : 表示控制LED2灯
    kbuf[0] = 3 : 表示控制LED3灯
    kbuf[1] = 0 :表示熄灭LED灯
    kbuf[1] = 1 :表示点亮LED灯
*/
ssize_t myled_write (struct file *file, const char __user *ubuf, size_t size, loff_t *offset)
{
    int ret;
    printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
     // 校准拷贝数据的大小
    if (size > sizeof(kbuf)) size = sizeof(kbuf);
    // 将用户空间的数据拷贝到内核空间
    ret = copy_from_user(kbuf,ubuf,size);
    if (ret) {
        printk("user space copy data to kernel space failed!\n");
        return -EINVAL;
    }
    switch (kbuf[0]) {
    case 1:
        if (kbuf[1] == 1) 
            *gpioe_odr_virl_addr |= (0x1 << 10);
        else
            *gpioe_odr_virl_addr &= (~(0x1 << 10));
        break;
    case 2:
        break;
    case 3:
        break;
    default:
        break;
    }

    return size;
}
int myled_close (struct inode *inode, struct file *file)
{
    printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
    return 0;
}

struct file_operations fops = {
    .open = myled_open,
    .read = myled_read,
    .write = myled_write,
    .release = myled_close,
};
// 完成led灯的初始化,
int led_init(void)
{
    int ret = 0;
    // 完成物理地址到虚拟地址的映射
    rcc_virl_addr = ioremap(RCC_MP_AHB4ENSETR_PHY_ADDR, 4);
    if (rcc_virl_addr == NULL) 
    {
        printk("memory map failed!\n");
        ret = -ENOMEM;
    }
    gpioe_moder_virl_addr = ioremap(GPIOE_MODER_PHY_ADDR, 4);
    if (gpioe_moder_virl_addr == NULL) 
    {
        printk("memory map failed!\n");
        ret = -ENOMEM;
    }
    gpioe_otyper_virl_addr = ioremap(GPIOE_OTYPER_PHY_ADDR, 4);
    if (gpioe_otyper_virl_addr == NULL) 
    {
        printk("memory map failed!\n");
        ret = -ENOMEM;
    }
    gpioe_ospeedr_virl_addr = ioremap(GPIOE_OSPEEDR_PHY_ADDR, 4);
    if (gpioe_ospeedr_virl_addr == NULL) 
    {
        printk("memory map failed!\n");
        ret = -ENOMEM;
    }
    gpioe_pupdr_virl_addr = ioremap(GPIOE_PUPDR_PHY_ADDR, 4);
    if (gpioe_pupdr_virl_addr == NULL) 
    {
        printk("memory map failed!\n");
        ret = -ENOMEM;
    }
    gpioe_odr_virl_addr = ioremap(GPIOE_ODR_PHY_ADDR, 4);
    if (gpioe_odr_virl_addr == NULL) 
    {
        printk("memory map failed!\n");
        ret = -ENOMEM;
    }
    // 使用gpioe外设的时钟
    *rcc_virl_addr = (0x1 << 4);
    // 初始化GPIO引脚为输出,推挽输出,低俗模式,禁止上拉下拉,默认输出低电平
    *gpioe_moder_virl_addr &= (~(0x3 << 20));
    *gpioe_moder_virl_addr |= (0x1 << 20);
    *gpioe_otyper_virl_addr &= (~(0x1 << 10));
    *gpioe_ospeedr_virl_addr &= (~(0x3 << 20));
    *gpioe_pupdr_virl_addr &= (~(0x3 << 20));
    *gpioe_odr_virl_addr &= (~(0x1 << 10));
    return ret;
}
// 取消led灯的初始化
void led_delInit(void)
{
    iounmap(gpioe_odr_virl_addr);
    iounmap(gpioe_pupdr_virl_addr);
    iounmap(gpioe_ospeedr_virl_addr);
    iounmap(gpioe_otyper_virl_addr);
    iounmap(gpioe_moder_virl_addr);
    iounmap(rcc_virl_addr);
}
static int __init myled_init(void)
{
    int ret;
    // 注册字符设备
    major = register_chrdev(0, MYNAME, &fops);
    if (major < 0) {
        printk("register char device failed!\n");
        return -EIO;
    }

    ret = led_init();
    if (ret < 0) {
        printk("led init failed!\n");
        return ret;
    }
    return 0;
}

static void __exit myled_exit(void)
{
    led_delInit();
    unregister_chrdev(major, MYNAME);
}
module_init(myled_init);
module_exit(myled_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("zk <zhouk_bj@hqyj.com>");

3.3.4 编写led灯的应用程序


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

char u_buf[128] = {0};
int main(int argc, const char *argv[])
{
    /*your code*/
    int fd;
    fd = open("/dev/myled", O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }
    while(1) {
        u_buf[0] = 1;
        u_buf[1] = 1;
        write(fd, u_buf, sizeof(u_buf));
        sleep(1);
        u_buf[1] = 0;
        write(fd, u_buf, sizeof(u_buf));
        sleep(1);
    }
    close(fd);
    return 0;
}

3.3.5 测试led灯的驱动和应用程序

1> 编译led灯的驱动及应用程序:make arch=arm modname=myled  all
2> 拷贝驱动程序及led灯的应用程序到跟文件系统中:make install
3> 完成开发板开发阶段的系统的启动
4> 加载驱动 : 
	[root@fsmp1a /] insmod myled.ko
5> 查看驱动对应的主设备号 : 
	[root@fsmp1a /] cat /proc/devices, 需要查看自己的主设备号
6> 在/dev目录下创建设备文件 
	[root@fsmp1a /] mknod  /dev/myled c  242  0 
7> 运行应用程序: 	
	[root@fsmp1a /] ./test 
8> 观看led灯的现象
9> 卸载驱动  
	[root@fsmp1a /] rmmod  mycdev
;
    }
    while(1) {
        u_buf[0] = 1;
        u_buf[1] = 1;
        write(fd, u_buf, sizeof(u_buf));
        sleep(1);
        u_buf[1] = 0;
        write(fd, u_buf, sizeof(u_buf));
        sleep(1);
    }
    close(fd);
    return 0;
}

3.3.5 测试led灯的驱动和应用程序

1> 编译led灯的驱动及应用程序:make arch=arm modname=myled  all
2> 拷贝驱动程序及led灯的应用程序到跟文件系统中:make install
3> 完成开发板开发阶段的系统的启动
4> 加载驱动 : 
	[root@fsmp1a /] insmod myled.ko
5> 查看驱动对应的主设备号 : 
	[root@fsmp1a /] cat /proc/devices, 需要查看自己的主设备号
6> 在/dev目录下创建设备文件 
	[root@fsmp1a /] mknod  /dev/myled c  242  0 
7> 运行应用程序: 	
	[root@fsmp1a /] ./test 
8> 观看led灯的现象
9> 卸载驱动  
	[root@fsmp1a /] rmmod  mycdev
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值