简单字符驱动笔记(朱有鹏)

本文深入讲解字符设备驱动的实现过程,包括驱动初始化、文件操作结构体定义、注册与注销流程等关键技术点。并通过实例演示如何在Linux环境下创建并管理字符设备。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

框架

static int __init chrdev init(void)
{
    printk(KERN_DEBUG "chrdev_init");
    return 0;
}

static void __exit chrdev_exit(void)
{
    printk(KERN_DEBUG ""chrdev_exit);
}



module_init(chrdev_init);
module_exit(chrdev_exit);


MODULE_LICENSE("GPL");              // 描述模块的许可证
MODULE_AUTHOR("aston");				// 描述模块的作者
MODULE_DESCRIPTION("module test");	// 描述模块的介绍信息
MODULE_ALIAS("alias xxx");			// 描述模块的别名信息

insmod与module_init宏。模块源代码中用module_init宏声明了一个函数(在我们这个例子里是chrdev_init函数),作用就是指定chrdev_init这个函数和insmod命令绑定起来,也就是说当我们insmod module_test.ko时,insmod命令内部实际执行的操作就是帮我们调用chrdev_init函数。

printk相比printf来说还多了个:打印级别的设置。printk的打印级别是用来控制printk打印的这条信息是否在终端上显示的。应用程序中的调试信息要么全部打开要么全部关闭,一般用条件编译来实现(DEBUG宏),但是在内核中,因为内核非常庞大,打印信息非常多,有时候整体调试内核时打印信息要么太多找不到想要的要么一个没有没法调试。所以才有了打印级别这个概念。
操作系统的命令行中也有一个打印信息级别属性,值为0-7。当前操作系统中执行printk的时候会去对比printk中的打印级别和我的命令行中设置的打印级别,小于我的命令行设置级别的信息会被放行打印出来,大于的就被拦截的。譬如我的ubuntu中的打印级别默认是4,那么printk中设置的级别比4小的就能打印出来,比4大的就不能打印出来。ubuntu中这个printk的打印级别控制没法实践,ubuntu中不管你把级别怎么设置都不能直接打印出来,必须dmesg命令去查看。

照此分析,那insmod时就应该能看到chrdev_init中使用printk打印出来的一个chrdev_init字符串,但是实际没看到。原因是ubuntu中拦截了,要怎么才能看到呢?在ubuntu中使用dmesg命令就可以看到了。

 

定义一个struct file_operation 结构体

file_operations结构体
(1)元素主要是函数指针,用来挂接实体函数地址,驱动源码中提供真正的open、read、write、close等函数实体
(2)每个设备驱动都需要一个该结构体类型的变量
(3)设备驱动向内核注册时提供该结构体类型的变量

 

注册驱动程序register_chrdev   (#include <linux/fs.h>)

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

(1)作用,驱动向内核注册自己的file_operations
(2)参数
(3)inline和static

 

内核如何管理字符设备驱动
(1)内核中有一个数组用来存储注册的字符设备驱动
(2)register_chrdev内部将我们要注册的驱动的信息(主要是 )存储在数组中相应的位置
(3)cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动)
(4)好好理解主设备号(major)的概念

注册字符设备驱动
(1)为何要注册驱动
(2)谁去负责注册
(3)向谁注册
(4)注册函数从哪里来
(5)注册前怎样?注册后怎样?注册产生什么结果?

谁来调用驱动,驱动入口函数 first_drv_init()

修饰入口函数module_init(first_drv_init)

 

自定义一个file_operation结构体变量,并且去填充

 

static const struct file_operations test_fops = {
    .ower    = THIS_MODULE,
    .open    = test_chrdev_open,
    .release = test_chrdev_release,
};

原型int (*open) (struct inode *, struct file *);

写test_chrdev_open函数

static int test_chrdev_open(struct inode *inode,struct file *file)
{
    printk(KERN_INFO "test_chrdev_open\n");

    return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "test_chredev_open\n");

    return 0;
}

在module_init 宏调用的函数中去注册字符设备驱动register_chrdev

#define MYMAJOR    200

#define MYNAME      "testchar"

原型:static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)

//模块安装函数
static int __init chrdev_init(void)
{
    int ret = -1;

    printk(KERN INFO "chrdev init helloworld init\n");
    
    //在module_init宏调用的函数中去注册字符设备驱动
    ret = register_chrdev(MYMAJOR, MYNAME, &test_fops);
    if(ret)
    {
        printk(KERN_ERR "register_chrdev fail\n");
        return -EINVAL;
    }
    printk(KERN_INFO "register_chrdev success\n");

    return 0;
}

在module exit宏调用的函数中去注销字符设备驱动

static void __exit chrdev_exit(void)
{
    printk(KERN_INFO "chrdev_exit helloworld exit\n");

    //在module_exit 宏调用的函数中去注销字符设备驱动

    unregister_chrdev(MYMAJOR,MYNAME);
}

 

驱动设备文件的创建
(1)何为设备文件
(2)设备文件的关键信息是:设备号 = 主设备号 + 次设备号,使用ls -l去查看设备文件,就可以得到这个设备文件对应的主次设备号。
(3)使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号

 

写应用来测试驱动

#include <stdio.h>

#define FILE "/dev/test"      //刚才mknod 创建的设备文件名

int main(void)
{
    int fd = -1;
    

    fd = open(FILE, O_RDWR);
    if(fd < 0)
    {
        printf("open %s error.\n",FILE);
        return -1;
    }
    printf("open %s success\n",FILE);

//读写文件


//关闭文件
    
    return 0;
}

完整驱动:

 

#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init   __exit
#include <linux/fs.h>

#define MYMAJOR		200
#define MYNAME		"testchar"

int mymajor;


static int test_chrdev_open(struct inode *inode, struct file *file)
{
	// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
	// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。
	printk(KERN_INFO "test_chrdev_open\n");
	
	return 0;
}

static int test_chrdev_release(struct inode *inode, struct file *file)
{
	printk(KERN_INFO "test_chrdev_release\n");
	
	return 0;
}

// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
	.owner		= THIS_MODULE,				// 惯例,直接写即可
	
	.open		= test_chrdev_open,			// 将来应用open打开这个设备时实际调用的
	.release	= test_chrdev_release,		// 就是这个.open对应的函数
};


// 模块安装函数
static int __init chrdev_init(void)
{	
	printk(KERN_INFO "chrdev_init helloworld init\n");

	// 在module_init宏调用的函数中去注册字符设备驱动
	// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
	// 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数
	mymajor = register_chrdev(0, MYNAME, &test_fops);
	if (mymajor < 0)
	{
		printk(KERN_ERR "register_chrdev fail\n");
		return -EINVAL;
	}
	printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);

	return 0;
}

// 模块下载函数
static void __exit chrdev_exit(void)
{
	printk(KERN_INFO "chrdev_exit helloworld exit\n");
	
	// 在module_exit宏调用的函数中去注销字符设备驱动
	unregister_chrdev(mymajor, MYNAME);
	
}


module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");				// 描述模块的许可证
MODULE_AUTHOR("aston");				// 描述模块的作者
MODULE_DESCRIPTION("module test");	// 描述模块的介绍信息
MODULE_ALIAS("alias xxx");			// 描述模块的别名信息








 

在module_init宏调用的函数中去注册字符设备驱动
major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数

#include <linux/module.h>        // module_init  module_exit
#include <linux/init.h>            // __init   __exit

 

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

copy_from_user函数的返回值定义,和常规有点不同。返回值如果成功复制则返回0,如果 不成功复制则返回尚未成功复制剩下的字节数。

#include <asm/uaccess.h>                  //copy_to_user       copy_from_user

 

驱动中如何操控硬件
还是那个硬件
(1)硬件物理原理不变
(2)硬件操作接口(寄存器)不变
(3)硬件操作代码不变
哪里不同了?
(1)寄存器地址不同。原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。

(2)编程方法不同。裸机中习惯直接用函数指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性。

 

内核中有2套虚拟地址映射方法:动态和静态
(1)静态映射方法的特点:
    内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核
    在内核启动时(开机)建立静态映射表,到内核关机时销毁,中间一直有效
    对于移植好的内核,你用不用他都在那里
(2)动态映射方法的特点:
    驱动程序根据需要随时动态的建立映射、使用、销毁映射
    映射是短期临时的

如何选择虚拟地址映射方法
(1)2种映射并不排他,可以同时使用
(2)静态映射类似于C语言中全局变量,动态方式类似于C语言中malloc堆内存
(3)静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射(还得学会使用那些内核函数的使用)

 

关于静态映射要说的
(1)不同版本内核中静态映射表位置、文件名可能不同
(2)不同SoC的静态映射表位置、文件名可能不同
(3)所谓映射表其实就是头文件中的宏定义

 

三星版本内核中的静态映射表
(1)主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h
CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。所以内核在定义寄存器地址时都是先找到基地址,然后再用基地址+偏移量来寻找具体的一个寄存器。
map-s5p.h中定义的就是要用到的几个模块的寄存器基地址。
map-s5p.h中定义的是模块的寄存器基地址的虚拟地址。
(2)虚拟地址基地址定义在:arch/arm/plat-samsung/include/plat/map-base.h
#define S3C_ADDR_BASE    (0xFD000000)        // 三星移植时确定的静态映射表的基地址,表中的所有虚拟地址都是以这个地址+偏移量来指定的
(3)GPIO相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h
表中是GPIO的各个端口的基地址的定义
(4)GPIO的具体寄存器定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h

 

静态映射操作LED2
参考裸机中的操作方法添加LED操作代码
(1)宏定义
(2)在init和exit函数中分别点亮和熄灭LED
实践测试
(1)insmod和rmmod时观察LED亮灭变化
(2)打印出寄存器的值和静态映射表中的分析相对比
5.2.15.3、将代码移动到open和close函数中去


静态映射操作LED3
添加驱动中的写函数
(1)先定义好应用和驱动之间的控制接口,这个是由自己来定义的。譬如定义为:应用向驱动写"on"则驱动让LED亮,应用向驱动写"off",驱动就让LED灭
(2)应用和驱动的接口定义做的尽量简单,譬如用1个字目来表示。譬如定义为:应用写"1"表示灯亮,写"0"表示让灯灭。
5.2.16.2、写应用来测试写函数
5.2.16.3、驱动和应用中来添加读功能


5.2.17.动态映射操作LED
5.2.17.1、如何建立动态映射
(1)request_mem_region,向内核申请(报告)需要映射的内存资源。
(2)ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址
5.2.17.2、如何销毁动态映射
(1)iounmap
(2)release_mem_region
注意:映射建立时,是要先申请再映射;然后使用;使用完要解除映射时要先解除映射再释放申请。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值