底软驱动 | Linux字符设备驱动开发基础

文章目录

  • 知识整理--Linux字符设备驱动开发基础
    • 字符设备基础1
      • 从一个最简单的模块源码说起
      • 字符设备驱动工作原理
      • 字符设备驱动代码实践--给空模块添加驱动壳子
      • 应用程序如何调用驱动
    • 字符设备基础2
      • 添加读写接口(应用和驱动之间的数据交换)
      • 驱动中如何操控硬件(和裸机代码有何不同)
      • 静态映射和动态映射
        • 静态映射操作LED
        • 动态映射操作LED
    • 字符设备基础3
      • 字符设备驱动注册新接口cdev
        • 结构体cdev与相关的操作函数介绍
        • 中途出错的倒影式处理方法
        • 使用cdev_alloc
      • 字符设备驱动注册代码分析
        • register_chrdev
        • register_chrdev_region/alloc_chrdev_region
      • 总结:字符设备驱动在内核中如何调用(1)
      • 自动创建和删除设备文件
      • 关于sys文件系统
    • 内核提供的读写寄存器接口
    • 总结
  • 原文链接

知识整理–Linux字符设备驱动开发基础

我理解的linux驱动:封装对底层硬件的操作,向上层应用提供操作接口

文中有些地方没贴出相应的函数原型,请自行查阅,或者用SouceInsight搜索自己的内核源码树(本人就是用该方式查阅函数的使用)简单设备驱动开发基础知识,暂不考虑驱动框架。文章根据GFM排版https://github.com/TongxinV

开发环境的搭建:内核源码树、nfs挂载的roofs、开发配置好相应的bootcmdbootargs

驱动开发的步骤:1.驱动源码代码的编写、Makefile文件编写、编译得到;2.insmod装载模块、测试,rmmod卸载模块。

bootcmd和bootargs

1.设置bootcmd使开发板能够通过tftp下载自己建立的内核源码树编译得到的zImageset bootcmd 'tftp 0x30008000 zImage;bootm 0x30008000'
(注:bootcmd=movi read kernel 30008000; movi read rootfs 30B00000 300000; bootm 30008000 30B00000 这样的bootcmd是从inand启动内核的时候用的)2.设置bootargs使开发板从nfs去挂载rootfs(内核配置记得打开使能nfs形式的rootfs)
setenv bootargs root=/dev/nfs nfsroot=192.168.1.141:/root/x210_porting/rootfs/rootfs ip=192.168.1.10:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off  init=/linuxrc console=ttySAC2,115200 

编译驱动源码的Makefile文件

#ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2
#KERN_VER = $(shell uname -r)
#KERN_DIR = /lib/modules/$(KERN_VER)/build	# 开发板的linux内核的源码树目录,根据自己在源码树存放的目录修改
KERN_DIR = /root/driver/kernel
obj-m	+= module_test.o     //-m 表示我们要将module_test.c编译成一个模块//-y表示我们要将module_test.c编译链接进zImage
all:make -C $(KERN_DIR) M=`pwd` modules //-C 表示进入到某一个目录下去编译//`pwd`:表示把两个`号中间的内容当成命令执行//M=`pwd`则表示把pwd打印的内容保存起来,目的是为了编译好了之后能够返回原来的目录//modules就是真正用来编译模块的命令,在内核的其他地方定义过了
cp:									cp *.ko /root/porting_x210/rootfs/rootfs/driver_test.PHONY: clean	//把clean当成一个伪目标
clean:make -C $(KERN_DIR) M=`pwd` modules clean

总结:模块的makefile非常简单,本身并不能完成模块的编译,而是通过make -C进入到内核源码树下借用内核源码的体系来完成模块的编译链接的。


文章目录

  • 知识整理--Linux字符设备驱动开发基础
    • 字符设备基础1
      • 从一个最简单的模块源码说起
      • 字符设备驱动工作原理
      • 字符设备驱动代码实践--给空模块添加驱动壳子
      • 应用程序如何调用驱动
    • 字符设备基础2
      • 添加读写接口(应用和驱动之间的数据交换)
      • 驱动中如何操控硬件(和裸机代码有何不同)
      • 静态映射和动态映射
        • 静态映射操作LED
        • 动态映射操作LED
    • 字符设备基础3
      • 字符设备驱动注册新接口cdev
        • 结构体cdev与相关的操作函数介绍
        • 中途出错的倒影式处理方法
        • 使用cdev_alloc
      • 字符设备驱动注册代码分析
        • register_chrdev
        • register_chrdev_region/alloc_chrdev_region
      • 总结:字符设备驱动在内核中如何调用(1)
      • 自动创建和删除设备文件
      • 关于sys文件系统
    • 内核提供的读写寄存器接口
    • 总结
  • 原文链接

字符设备基础1

从一个最简单的模块源码说起

#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init   __exit
// 模块安装函数
static int __init chrdev_init(void)
{	printk(KERN_INFO "chrdev_init helloworld init\n");return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{printk(KERN_INFO "chrdev_exit helloworld exit\n");
}
module_init(chrdev_init);
module_exit(chrdev_exit);// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");				// 描述模块的许可证

(1)使用printk打印调试信息,printk可以设置打印级别。常见的KERN_DBUG-8\KERN_INFO-7,当前系统也有一个打印信息的级别0-7(比如当前系统打印信息的级别为4,则printk打印小于级别4)。

查看当前系统打印信息的级别:cat /proc/sys/kernel/printk;修改:echo 8 > /proc/sys/kernel/printk

(2)驱动源代码中包含的头文件和原来应用编程程序中包含的头文件不是一回事。应用编程中包含的头文件是应用层的头文件,是应用程序的编译器带来的(譬如gcc的头文件路径在/usr/include下,这些东西是和操作系统无关的)。驱动源码属于内核源码的一部分,驱动源码中的头文件其实就是内核源代码目录下的include目录下的头文件。

(3)函数修饰符__init(前面加下划线的表示这是给内核使用的函数),本质上是个宏定义,在内核源代码中就有#define __init xxxx。这个__init的作用就是将被他修饰的函数放入.init.text段中去(本来默认情况下函数是被放入.text段中)。

#define __init	__section(.init.text) __cold notrace├──#define __section(S) __attribute__ ((__section__(#S)))              

整个内核中的所有的这类函数都会被链接器根据链接脚本放入.init.text段中,所以所有的内核模块的__init修饰的函数其实是被统一放在一起的。
内核启动时统一会加载.init.text段中的这些模块安装函数,加载完后就会把这个段给释放掉以节省内存。__exit同理。

字符设备驱动工作原理

可以理解模块是一种机制,驱动使用了模块这种机制来实现

系统整体工作原理:(1)应用层->API->设备驱动->硬件;(2)API:open、read、write、close等;(3)驱动源码中提供真正的open、read、write、close等函数实体

file_operations结构体(另外一种为attribute方式后面再讲):(1)元素主要是函数指针,用来挂接实体函数地址;(2)每个设备驱动都需要一个该结构体类型的变量;(3)设备驱动向内核注册时提供该结构体类型的变量。

注册字符设备驱动register_chrdev

    static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops){return __register_chrdev(major, 0, 256, name, fops);}

(1)作用,驱动向内核注册自己的file_operations结构体,注册的过程其实主要是将要注册的驱动的信息存储在内核中专门用来存储注册的字符设备驱动的数组中相应的位置

(2)参数:设备号major–major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备,内核如果成功分配就会返回分配的主设备号;如果分配失败会返回负数

(3)inline和static

inline:当把函数定义在头文件里面的时候,如果你这个头文件被两个及两个以上的函数包含的时候,在链接的时候就会出错。inline的作用就是解决这个问题,原地展开并能够实现静态检查。另外一个原因是函数本身就比较短。

内核如何管理字符设备驱动

(1)内核中用一个数组来存储注册的字符设备驱动;(2)register_chrdev内部将我们要注册的驱动的信息(fops结构体地址)存储在数组中相应的位置;(3)cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动)

字符设备驱动代码实践–给空模块添加驱动壳子

核心工作:定义file_operations类型变量及其元素填充、注册驱动

简单的驱动程序示例

module_test.c├── 模块安装函数xxx│   └── 注册字符设备驱动register_chrdev(MYNMAJOR, MYNAME, &test_module_fops)├── 模块安装函数yyy│   └── 注销字符设备驱动unregister_chrdev(MYNMAJOR, MYNAME)│   ├── module_init(模块安装函数xxx);├── module_exit(模块卸载函数yyy);│     └── MODULE_LICENSE("GPL");
#include <linux/module.h>  // module_init  module_exit
#include <linux/init.h>    // __init   __exit
#include <linux/fs.h>      // file_operations   没写会报错:xxx has initializer but 								incomplete type#define MYNMAJOR  200
#define MYNAME    "test_chrdev"//file_operations结构体变量中填充的函数指针的实体,函数的格式要遵守
static int test_chrdev_open(struct inode *inode, struct file *file)
{//这个函数中真正应该放置的是打开这个设备的硬件操作代码部分//但是现在我们暂时写不了那么多,所以就就用一个printk打印个信息来做代表 printk(KERN_INFO "test_module_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_module_fops = {.owner		= THIS_MODULE,         //惯例,所有的驱动都有这一个,这也是这结构体中唯一一个不是函数指针的元素.open		  = test_chrdev_open,    //将来应用open打开这个这个设备时实际调用的函数.release	= test_chrdev_release,   //对应close,为什么不叫close呢?详见后面release和close的区别的讲解
};/*********************************************************************************/
// 模块安装函数
static int __init chrdev_init(void)
{printk(KERN_INFO "chrdev_init helloworld init\n");//在module_init宏调用的函数中去注册字符设备驱动int ret = -1;     //register_chrdev 返回值为int类型ret = register_chrdev(MYNMAJOR, MYNAME, &test_module_fops);//参数:主设备号major,设备名称name,自己定义好的file_operations结构体变量指针,注意是指针,所以要加上取地址符//完了之后检查返回值if(ret){printk(KERN_ERR "register_chrdev fial\n");  //注意这里不再用KERN_INFOreturn -EINVAL; //内核中定义了好多error number 不都用以前那样return -1;负号要加 !!}printk(KERN_ERR "register_chrdev success...\n");return 0;
}// 模块卸载函数
static void __exit chrdev_exit(void)
{printk(KERN_INFO "chrdev_exit helloworld exit\n");//在module_exit宏调用的函数中去注销字符设备驱动//实验中,在我们这里不写东西的时候,rmmod 后lsmod 查看确实是没了,但是cat /proc/device发现设备号还是被占着unregister_chrdev(MYNMAJOR, MYNAME);  //参数就两个//检测返回值......return 0;
}
/*********************************************************************************/module_init(chrdev_init);        //insmod 时调用
module_exit(chrdev_exit);        //rmmod  时调用// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");		      // 描述模块的许可证

应用程序如何调用驱动

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

注:不可能总用mknod来创建设备文件,能否自动生成和删除设备文件?linux内核有一种机制–udev(嵌入式中用的是mdev)后面细讲

一个简单的应用程序示例app.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>        //man 2 open 查看头文件有哪些
#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);// 读写文件	...// 关闭文件close(fd);	return 0;
}


字符设备基础2

添加读写接口(应用和驱动之间的数据交换)

照猫画虎

在驱动程序中添加:

    ssize_t test_chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)//struct file *file:指向我们要操作的文件;const char __user *buf:用户空间的buf{printk(KERN_INFO "test_chrdev_read\n");......static ssize_t test_chrdev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos){printk(KERN_INFO "test_chrdev_write\n");......

在应用程序中添加:

//读写文件
read(fd,buf,100);//最后一个参数为要读取的字节数
write(fd,"helloworld",10);
......

测试:测试前先rmmod 把之前实验的模块卸载掉,lsmod确认下 cat /proc/devices再insmod;还有设备文件也要rm /dev/xxx删设备文件,安装完模块后再mknod重新建立设备文件。然后执行应用程序查看打印信息(在后面我们会讲怎么弄才不会那么麻烦)

应用和驱动之间的数据交换:

写函数的本质就是将应用层传递的过来的数据先复制到内核中,然后将之以正确的方式写入硬件,完成操作
目前接触到的就两种:copy_from_user、copy_to_user和mmap,这里只讲第一种

完成write和read函数:

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

module_test.c:


char kbuf[100];//内核空间的一个buf
......
static ssize_t test_chrdev_write(struct file *file, const char __user *buf,	size_t count, loff_t *ppos)
{int ret = -1;printk(KERN_INFO "test_chrdev_write\n");//使用该函数将应用层的传过来的ubuf中的内容拷贝到驱动空间(内核空间)的一个buf中//memcpy(kbuf,ubuf);     //不行,因为2个不在一个地址空间中menset(kbuf, 0, sizeof(kbuf));ret = copy_from_user(kbuf,ubuf,count);if(ret){printk(KERN_ERR "copy_from_user fail\n");return -EINVAL;//在真正的的驱动中没复制成功应该有一些纠错机制,这里我们简单点}printk(KERN_ERR "copy_from_user success..\n");//到这里我们就成功把用户空间的数据转移到内核空间了//真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据去写硬件完成硬件的操作//所以下面就应该是操作硬件的代码......return 0;
}ssize_t test_chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{int ret = -1;printk(KERN_INFO "test_chrdev_read\n");ret = copy_to_user(ubuf,kbuf,size);if(ret){printk(KERN_ERR "copy_to_user fail\n");return -EINVAL;//在真正的的驱动中没复制成功应该有一些纠错机制,这里我们简单点}printk(KERN_ERR "copy_to_user success..\n");return 0;
}

app.c

......
//读写文件
write(fd, “helloworld”, 10);
read(fd,buf,100);
printf(“读出来的内容是:%s \n”,buf);
打印结果:...

驱动中如何操控硬件(和裸机代码有何不同)

在PowerPC、m68k和ARM等体系中,外设I/O端口具有与内存一样的物理地址,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。Linux的驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将物理地址映射到内核虚地址空间。

还是那个硬件:

(1)硬件物理原理不变;(2)硬件操作接口(寄存器)不变;(3)硬件操作代码不变

哪里不同:

(1)寄存器地址不同。原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。
(2)编程习惯不同。裸机中习惯直接用函数指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性。

内核的虚拟地址映射方法:

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

如何选择虚拟地址映射方法:

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

静态映射和动态映射

在ARM 存储系统中,使用内存管理单元(MMU)实现虚拟地址到实际物理地址的映射。MMU的实现过程实际上就是一个查表映射的过程。建立页表是实现MMU功能不可缺少的一步。页表位于系统的内存中,页表的每一项对应于一个虚拟地址到物理地址的映射。每一项的长度即是一个字的长度(在32位ARM中,一个字的长度被定义为4B)。页表项除完成虚拟地址到物理地址的映射功能之外,还定义了访问权限和缓冲特性等。

由于篇幅有限,这里只分析如何使用(以s5pv210为例),具体如何实现点击这里Linux内核静态映射表建立过程分析

静态映射操作LED

关于静态映射要说的:

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

三星版本内核中的静态映射表:

(1)主映射表位于:arch/arm/plat-samsung/include/plat/map-base.harch/arm/plat-s5p/include/plat/map-s5p.h

map-base.h

...
#define S3C_ADDR_BASE	(0xFD000000)//三星移植时确定的静态映射表的基地址,表中的所有虚拟地址都是以这个地址+偏移量来指定的#ifndef __ASSEMBLY__
#define S3C_ADDR(x)	((void __iomem __force *)S3C_ADDR_BASE + (x))
#else
#define S3C_ADDR(x)	(S3C_ADDR_BASE + (x))
#endif#define S3C_VA_IRQ	S3C_ADDR(0x00000000)	/* irq controller(s) */
#define S3C_VA_SYS	S3C_ADDR(0x00100000)	/* system control */
#define S3C_VA_MEM	S3C_ADDR(0x00200000)	/* memory control */
...
#define S5P_VA_GPIO S3C_ADDR(0x00500000) 
...

map-base.h和map-s5p.h中定义的是各模块的寄存器基地址的虚拟地址。(后面那个可能是九鼎根据自己的硬件自己移植)

CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。所以内核在定义寄存器地址时都是先找到基地址,然后再用基地址+偏移量来寻找具体的一个寄存器。(map-s5p.h中定义的就是要用到的几个模块的寄存器基地址。并没有全,三星只写了自己要用的。将来实际工作如果要用到的这里没有就自己添加)

(2)GPIO各个端口相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h表中是GPIO的各个端口的基地址的定义。GPIO还分成GPA0、GPA1、GPB0、GPC、E、F、G、H等

regs-gpio.h

/* Base addresses for each of the banks */
#define S5PV210_GPA0_BASE		(S5P_VA_GPIO + 0x000)
#define S5PV210_GPA1_BASE		(S5P_VA_GPIO + 0x020)
#define S5PV210_GPB_BASE 		(S5P_VA_GPIO + 0x040)
#define S5PV210_GPC0_BASE		(S5P_VA_GPIO + 0x060)
...

(3)每一个GPIO的具体寄存器定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h

gpio-bank.h

...
#define S5PV210_GPA0CON			(S5PV210_GPA0_BASE + 0x00)
#define S5PV210_GPA0DAT			(S5PV210_GPA0_BASE + 0x04)
#define S5PV210_GPA0PUD			(S5PV210_GPA0_BASE + 0x08)
#define S5PV210_GPA0DRV			(S5PV210_GPA0_BASE + 0x0c)
#define S5PV210_GPA0CONPDN		(S5PV210_GPA0_BASE + 0x10)
#define S5PV210_GPA0PUDPDN		(S5PV210_GPA0_BASE + 0x14)
...

Q:为什么给个虚拟地址就能找到对应的物理地址?----MMU的存在,Linux内核静态映射表建立过程分析

驱动中添加相应代码:

#include <mach/regs-gpio.h>		//虚拟地址映射表
#include <mach/gpio-bank.h>	
...
#define rGPJ0CON	*((volatile unsigned int *)GPJ0CON)    //强制类型转化为指针类型,再解引用
#define rGPJ0DAT	*((volatile unsigned int *)GPJ0DAT)
...
//写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
{int ret = -1;printk(KERN_INFO "test_chrdev_write\n");// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中//memcpy(kbuf, ubuf);		// 不行,因为2个不在一个地址空间中memset(kbuf, 0, sizeof(kbuf));ret = copy_from_user(kbuf, ubuf, count);if (ret){printk(KERN_ERR "copy_from_user fail\n");return -EINVAL;}printk(KERN_INFO "copy_from_user success..\n");if (kbuf[0] == '1'){rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));}else if (kbuf[0] == '0'){rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));}	return 0;
}...

完整源代码module_test.c

注:我们驱动这么写既是正确的又是不正确的,正确的是说它能实现功能,不正确是说它写法不符合常规,常规的写法就是我们在驱动里只负责单纯的操作硬件,而应该把一些判断啊跟用户相关的业务逻辑写到应用里而不应该把它写到驱动里。
动态映射操作LED

如何建立动态映射:

(1)request_mem_region,向内核申请(报告)需要映射的内存资源。参数:寄存器物理地址,寄存器占用字节数,name
(2)ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址。参数:寄存器物理地址,寄存器占用字节数;返回值:映射到虚拟地址的地址的指针。

如何销毁动态映射

(1)iounmap
(2)release_mem_region注意:映射建立时,是要先申请再映射;然后使用;使用完要解除映射时要先解除映射再释放申请。(倒影式结构)

驱动中添加相应代码:

在模块安装中去申请资源和实现映射;在模块卸载中去解除映射和释放资源

...
#define GPJ0CON_PA	0xe0200240
#define GPJ0DAT_PA 	0xe0200244unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;
...// 模块安装函数
static int __init chrdev_init(void)
{	printk(KERN_INFO "chrdev_init helloworld init\n");// 在module_init宏调用的函数中去注册字符设备驱动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);// 使用动态映射的方式来操作寄存器if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))return -EINVAL;if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))return -EINVAL;pGPJ0CON = ioremap(GPJ0CON_PA, 4);pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);/***后面就可以通过pGPJ0CON、pGPJ0DAT来操作相应寄存器从而控制硬件了***/*pGPJ0CON = 0x11111111;*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));		// 亮return 0;
}// 模块下载函数
static void __exit chrdev_exit(void)
{printk(KERN_INFO "chrdev_exit helloworld exit\n");*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));	// 解除映射iounmap(pGPJ0CON);iounmap(pGPJ0DAT);release_mem_region(GPJ0CON_PA, 4);release_mem_region(GPJ0DAT_PA, 4);// 在module_exit宏调用的函数中去注销字符设备驱动unregister_chrdev(mymajor, MYNAME);
}

实现同时映射多个寄存器:

因为地址是挨着的,所以可以一次映射4n个字节的内存长度。怎么访问呢?–*(p+1),真实驱动中其实现方式为用结构体进行封装,具体点击这里动态映射之结构体方式操作寄存器


字符设备基础3

字符设备驱动注册新接口cdev

老接口:register_chrdev新接口:register_chrdev_region/alloc_chrdev_region + cdev

int register_chrdev_region(dev_t from, unsigned count, const char *name)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
结构体cdev与相关的操作函数介绍

include/linux/cdev.h

struct cdev {struct kobject kobj;struct module *owner;const struct file_operations *ops;  //fops结构体struct list_head list;dev_t dev;					//设备号(包含主设备号和次设备号);dev_t类型:typedef u_long dev_t;unsigned int count;
};

(1)对于这个结构体我们有很多函数来操作他,相关函数:cdev_alloccdev_initcdev_addcdev_del

(2)设备号:主设备号+次设备号

MKDEV:由一个主设备号和次设备号算出设备号
MAJOR:从设备号提取主设备号
MINOR:从设备号提取次设备号

代码示例:

新的接口注册字符设备驱动需要两步

module_test.c├── static struct cdev xxx_cdev;(定义一个全局cdev结构体)│├── 模块安装函数xxx│   ├── 第一步:注册/分配主次设备号  | 用register_chrdev_region/alloc_chrdev_region│   └── 第二步:注册字符设备驱动,cdev_init关联file_operations结构体 + cdev_add完成真正的注册├── 模块卸载函数yyy│   ├── 第一步:真正注销字符设备驱动 | 用cdev_del│   └── 第二步:注销申请的主次设备号unregister_chrdev_region│   ├── module_init(模块安装函数xxx);├── module_exit(模块卸载函数yyy);│ └── MODULE_LICENSE("GPL");
#define TEST_DEV MKDEV(MYMAJOR,0)
#define TEST_MAX 1
#define MYNAME    "test_chrdev"
...
static struct cdev test_cdev;   //定义一个全局cdev结构体
...
// 模块卸载函数
static void __exit chrdev_exit(void)
{...//第一步:注册/分配主次设备号 | 用register_chrdev_region/alloc_chrdev_region int retval;retval = register_chrdev_region(TEST_DEV, TEST_MAX, MYNAME);if (retval) {...//第二步:注册字符设备驱动cdev_init(&test_cdev, &test_fops);	//关联file_operations结构体retval = cdev_add(&test_cdev, TEST_DEV, TEST_MAX);//cdev_add完成真正的注册if (retval) {......
register_chrdev_region是在事先知道要使用的主、次设备号时使用的;要先cat /proc/devices去查看哪些可以使用的       

完整代码module_test.c,注意每个函数的参数的意义

中途出错的倒影式处理方法

return -EINVAL 这种错误处理方式是不合适的,譬如上面代码中第一步分配设备号执行正确了,第二步错了直接return,那第一步申请到的设备号就得不到注销,一直占着位置

使用cdev_alloc

从内存角度体会cdev_alloc用与不用的差别

module_test.c├── static struct cdev *pcdev;(定义一个全局cdev结构体类型指针)│├── 模块安装函数xxx│   ├── 第一步:注册/分配主次设备号  | 用register_chrdev_region/alloc_chrdev_region│   └── 第二步:注册字符设备驱动,cdev_alloc分配内存 + cdev_init关联file_operations结构体 + cdev_add完成真正的注册├── 模块卸载函数yyy│   ├── 第一步:真正注销字符设备驱动 | 用cdev_del│   └── 第二步:注销申请的主次设备号unregister_chrdev_region│   ├── module_init(模块安装函数xxx);├── module_exit(模块卸载函数yyy);│└── MODULE_LICENSE("GPL");

代码示例:

...
//static struct cdev test_cdev;  //简单粗暴不够灵活
static struct cdev *pcdev;		  //灵活
...
// 模块卸载函数
static void __exit chrdev_exit(void)
{...//第一步:注册/分配主次设备号 | 用register_chrdev_region/alloc_chrdev_region int retval;retval = register_chrdev_region(TEST_DEV, TEST_MAX, MYNAME);if (retval) {...//第二步:注册字符设备驱动pcdev = cdev_alloc();       //给pcdev分配内存,指针实例化,内部实际上是调用了内核自己的malloc--> kmalloccdev_init(pcdev, &test_fops); retval = cdev_add(pcdev, dev_id, TEST_COUNT);if (retval) {......
cdev_init有时候被下面两句代替:
pcdev->owner = THIS_MODULE;
pcdev->ops = &test_fops;
因为cdev_init内部的一些事情在你使用cdev_alloc时已经做了
但如果你使用全局变量的方式定义cdev结构体变量就一定要用cdev_init了

完整代码module_test.c,相对上一次改动:系统分配设备号 | 倒影式错误处理 | 使用cdev_alloc

字符设备驱动注册代码分析

register_chrdevregister_chrdev_region/alloc_chrdev_region

分析方法:看内核源码,RTFSC : Read The Fucking Source Code,工具SourceInsight

register_chrdev
register_chrdev__register_chrdev__register_chrdev_regioncdev_alloccdev_add
register_chrdev_region/alloc_chrdev_region
register_chrdev_region__register_chrdev_regionalloc_chrdev_region__register_chrdev_region

__register_chrdev_region分析:点击这里

总结:字符设备驱动在内核中如何调用(1)

目前为止的理解

自动创建和删除设备文件

之前讲的驱动一直需要使用mknod创建设备文件,能否自动生成和删除设备文件?

解决方案:udev(嵌入式中用的是mdev),应用层启用udev,内核驱动中使用相应接口

(1)什么是udev?应用层的一个应用程序;制作根文件系统时:/etc/init.d/rcS文件中有一行语句就是用来启用udev
(2)内核驱动和应用层udev之间有一套信息传输机制(netlink协议)
(3)驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除

内核驱动中使用相应接口:内核驱动设备类相关函数

(1)class_create

(2)device_create

编程示例:(以下为图片形式,因为自带的高亮并不能满足我想表达的,源码点击这里)

module_test.c├── static struct cdev  *pcdev;     (定义一个全局cdev 结构体类型指针)├── static struct class *test_class;(定义一个全局class结构体类型指针)│├── 模块安装函数xxx│   ├── 第一步:注册/分配主次设备号  | 用register_chrdev_region/alloc_chrdev_region│   ├── 第二步:注册字符设备驱动,cdev_alloc分配内存 + cdev_init关联file_operations结构体 + cdev_add完成真正的注册│   └── 添加设备类操作:第一步创建类class_create | 第二步创建用户空间下的设备文件device_create├── 模块卸载函数yyy│   ├── 删除用户空间下的设备文件和类文件:第一步device_destroy | 第二步class_destroy│   ├── 第一步:真正注销字符设备驱动 | 用cdev_del│   └── 第二步:注销申请的主次设备号unregister_chrdev_region│   ├── module_init(模块安装函数xxx);├── module_exit(模块卸载函数yyy);│└── MODULE_LICENSE("GPL");

class_createdevice_create内核源码分析

目前还不能够完全分析下来,这里只列出程序框架。日后有时间分析再贴上链接,自己也可以去内核看源码分析

class_create__class_create__class_registerkset_registerkobject_ueventdevice_createdevice_create_vargskobject_set_name_vargsdevice_registerdevice_initializedevice_addkobject_adddevice_create_filedevice_create_sys_dev_entrydevtmpfs_create_nodedevice_add_class_symlinksdevice_add_attrsdevice_pm_addkobject_uevent

关于sys文件系统

sys 文件系统是内核提供的一种调试方式,/sys/proc都属于我们的虚拟文件系统。虚拟文件系统有很多东西,是内核的数据结构,变量等,以文件的形式展示给我们。那么我们可以在应用层来跟这些文件进行交互实现跟内核的一些文件进行交互。

譬如用户空间/dev/..下的文件是通过内核的mknod或者设备类操作来添加上去,用户通过设备访问内核的驱动程序。

关于sys文件系统更详细的描述可以参考这篇文章使用 /sys 文件系统访问 Linux 内核

内核提供的读写寄存器接口

arm是IO与内存统一编址,其他平台如x86是IO与内存独立编址访问方式不一样,使用内核提供的寄存器读写接口具有可移植性

在文章随笔–Linux字符设备驱动开发基础前面写的驱动在静态映射操作寄存器,都用#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)的方式来访问寄存器,这样的做法在驱动中并不是很好,因为这样的做法在不同平台的情况下不具有可移植性。现在写的驱动是在ARM平台下去写的,ARM属于内存和IO统一编址的,在读写寄存器的时候即为进行IO操作,进行IO操作是和读写内存是一样的(IO也有个地址),这就叫统一编址。但是还有另外一些CPU(像x86)是非统一编址的,这种CPU在进行IO操作时的方法跟进行内存的读写的方法是不一样的。那么在这种情况下就有一种问题,如果写的驱动不仅要求在ARM下能够运行,还要求在X86下也要能够运行,如果还用#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)的方式显然是不合适的,需要进行比较大的修改。我们要怎样才能够使他能够具有很强的移植性呢?——内核已经帮我们想好了办法,即内核提供访问寄存器的读写接口(函数),使用这些函数具有可移植性。其实现的原理就是用条件编译,如下比较:

代码示例(静态映射):

...
#include <mach/regs-gpio.h>		//虚拟地址映射表
#include <mach/gpio-bank.h>	
#include <linux/io.h>
#include <linux/ioport.h>#define GPJ0CON		S5PV210_GPJ0CON
#define GPJ0DAT		S5PV210_GPJ0DAT
......writel(0x11111111, GPJ0CON);writel(((0<<3) | (0<<4) | (0<<5)), GPJ0DAT);...

代码示例(动态映射):

#include <linux/io.h>
#include <linux/ioport.h>
...
#define GPJ0CON_PA	0xe0200240#define S5P_GPJ0REG(x)		(x)
#define S5P_GPJ0CON		    S5P_GPJ0REG(0)
#define S5P_GPJ0DAT	        S5P_GPJ0REG(4)static void __iomem *baseaddr;	// 寄存器的虚拟地址的基地址,用来保存 ioremap的返回值
......if (!request_mem_region(GPJ0CON_PA, 8, "GPJ0BASE"))return -EINVAL;baseaddr = ioremap(GPJ0CON_PA, 8);writel(0x11111111, baseaddr + S5P_GPJ0CON);writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);...

总结

简单驱动一般框架(基础知识,不考虑驱动框架)

module_test.c├── MKDEV(MYMAJOR,0)或dev_t dev_id│├── static struct cdev  *pcdev;     (定义一个全局cdev 结构体类型指针)├── static struct class *test_class;(定义一个全局class结构体类型指针)│├── 自定义一个file_operations结构体变量,并且去填充│├── 模块安装函数xxx│   ├── 第一步:注册/分配主次设备号  | 用register_chrdev_region/alloc_chrdev_region│   ├── 第二步:注册字符设备驱动,cdev_alloc分配内存 + cdev_init关联file_operations结构体 + cdev_add完成真正的注册│   └── 添加设备类操作:第一步创建类class_create | 第二步创建用户空间下的设备文件device_create├── 模块卸载函数yyy│   ├── 删除用户空间下的设备文件和类文件:第一步device_destroy | 第二步class_destroy│   ├── 第一步:真正注销字符设备驱动 | 用cdev_del│   └── 第二步:注销申请的主次设备号unregister_chrdev_region│   ├── module_init(模块安装函数xxx);├── module_exit(模块卸载函数yyy);│└── MODULE_LICENSE("GPL");

还需要掌握1.添加读写接口(应用和驱动之间的数据交换);2.静态映射和动态映射(多个同时);3.倒影式结构;4.使用内核提供的读写寄存器接口

示例代码:module_test.c

原文链接

  • Linux字符设备驱动开发基础

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/46013.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WIFI连接阿里云

目录 1 实现功能 2 器件 3 AT指令 4 阿里云配置 4.1 打开阿里云 4.2 创建产品 4.3 添加设备 5 STM32配置 5.1 基础参数 5.2 功能定义 6 STM32代码 本文主要是记述一下&#xff0c;如何使用阿里云物联网平台&#xff0c;创建一个简单的远程控制小灯示例。 1 实现功能…

整洁架构SOLID-里氏替换原则(LSP)

文章目录 定义LSP继承实践正例反例 LSP软件架构实践反例 小结 定义 1988年&#xff0c;Barbara Liskov在描述如何定义子类型时写下了这样一段话&#xff1a; 这里需要的是一种可替换性&#xff1a;如果对于每个类型是S的对象o1都存在一个类型为T的对象o2&#xff0c;能使操作T…

Meta MobileLLM

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

鸿蒙开发—基础组件

目录 安装介绍 1.Text 2.Image 3.Button 4.Slider 安装介绍 该文章介绍鸿蒙开发中的部分基础组件&#xff0c;适用于鸿蒙开发的初学者。 1.软件下载地址&#xff1a;DevEco Studio-HarmonyOS Next Beta版-华为开发者联盟 (huawei.com) 2.安装DevEco Studio&#xff1a;…

最新版智能修图-中文luminar ai 1.55(13797) 和 neo1.20,支持m芯片和intel芯片(绝对可用)

Luminar AI for macOS 完整版本 这个程序是第一个完全由人工智能驱动的图像编辑器。有了它&#xff0c;创建引人注目的照片是有趣的&#xff0c;令人惊讶的容易。它是一个独立的照片编辑器和macOS插件。 Luminar AI for macOS 轻轻地塑造和完善一个肖像打造富有表现力的眼睛…

增加内容曝光、获得更多粉丝 - 「评论发红包」功能

目录 博客发放以及领取红包规则 1. 发布博客评论社区红包规则&#xff1a; 2. 博客评论红包领取规则 如何发红包评论&#xff1f; 发布红包评论益处 不知道大家有没有注意到&#xff0c;我们的「评论发红包」功能已经上线啦&#xff5e; 现在几乎所有的内容 -- 博客&…

TCP连接的三次握手和断开的四次挥手

TCP连接的建立过程通过三次握手完成&#xff0c;‌而连接的关闭过程则通过四次挥手完成。‌ 三次握手&#xff1a;‌这是TCP连接建立的过程&#xff0c;‌主要目的是确保双方都准备好进行数据传输。‌具体步骤如下&#xff1a;‌ 客户端向服务器发送一个SYN报文&#xff0c;‌请…

独立开发者系列(24)——使用redis

&#xff08;一&#xff09;REdis的使用原理 在早期的网站的时候&#xff0c;如果系统本身功能不是很复杂&#xff0c;比如就是内部的几个用户使用&#xff0c;而且基本就是汇报一点简单的设备维护信息&#xff0c;还有日常公告。完全可以不使用数据库&#xff0c;直接使用jso…

IoTDB 集群高效管理:一键启停功能介绍

如何快速启动、停止 IoTDB 集群节点的功能详解&#xff01; 在部署 IoTDB 集群时&#xff0c;对于基础的单机模式&#xff0c;启动过程相对简单&#xff0c;仅需执行 start-standalone 脚本来启动 1 个 ConfigNode 节点和 1 个 DataNode 节点。然而&#xff0c;对于更高级的分布…

02:项目二:感应开关盖垃圾桶

感应开关盖垃圾桶 1、PWM开发SG901.1、怎样通过C51单片机输出PWM波&#xff1f;1.2、通过定时器输出PWM波来控制SG90 2、超声波测距模块的使用3、感应开关盖垃圾桶 需要材料&#xff1a; 1、SG90舵机模块 2、HC-SR04超声波模块 3、震动传感器 4、蜂鸣器 5、若干杜邦线 1、PWM开…

7、y0usef

难度-低 局域网靶机地址发现 端口服务扫描 通过目录扫描发现adminstration目录&#xff0c;但是访问发现提升没有权限 尝试通过添加请求头X-Forwarded-For: http://127.0.0.1 成功绕过 访问发现是一个登录框 尝试admin admin发现成功登录。。。 发现文件上传功能点 尝试进…

JavaWeb后端学习

Web&#xff1a;全球局域网&#xff0c;万维网&#xff0c;能通过浏览器访问的网站 Maven Apache旗下的一个开源项目&#xff0c;是一款用于管理和构建Java项目的工具 作用&#xff1a; 依赖管理&#xff1a;方便快捷的管理项目以来的资源&#xff08;jar包&#xff09;&am…

鸿蒙系统在服装RFID管理中的应用:打造智能零售新时代

​随着物联网技术的迅速发展&#xff0c;服装零售行业正面临着新的变革与挑战。鸿蒙系统作为新一代智能操作系统&#xff0c;结合RFID技术&#xff0c;为服装行业提供了高效、智能的管理解决方案。常达智能物联&#xff0c;作为RFID技术的领先企业&#xff0c;致力于将鸿蒙系统…

Linux的世界 -- 初次接触和一些常见的基本指令

一、Linux的介绍和准备 1、简单介绍下Linux的发展史 1991年10月5日&#xff0c;赫尔辛基大学的一名研究生Linus Benedict Torvalds在一个Usenet新闻组(comp.os.minix&#xff09;中宣布他编制出了一种类似UNIX的小操作系统&#xff0c;叫Linux。新的操作系统是受到另一个UNIX的…

jenkins系列-04-jenkins参数化构建

使用maven build之前&#xff0c;先checkout 指定分支或标签&#xff1a; 拖拽调整顺序&#xff1a;shell执行在前&#xff0c;构建在后&#xff1a; gitee新建标签tag:

代理模式(大话设计模式)C/C++版本

代理模式 C #include <iostream> using namespace std;class Subject // Subject 定义了RealSubject和Proxy的共用接口..这样就在任何使用RealSubject的地方都可以使用Proxy { public:virtual void func(){cout << "Subject" << endl;} };class R…

头歌资源库(29)流水线最优调度

一、 问题描述 二、算法思想 这是一个经典的作业调度问题&#xff0c;可以使用动态规划来解决。 首先&#xff0c;我们可以将每个任务定义为一个节点&#xff0c;图中的边表示任务的先后顺序。根据题目的要求&#xff0c;每个任务必须先在印刷车间进行印刷&#xff0c;然后…

prometheus+grafana应用监控配置

配置Prometheus 官方地址&#xff1a;Download | Prometheus &#xff08;wegt下载压缩包&#xff0c;解压并重命名prometheus&#xff0c;文件放于/data/prometheus即可&#xff09; 配置 service方法(文件放于 /etc/systemd/system/prometheus.service)&#xff1a; [Unit…

k8s(四)---node

四、node node就是节点 1.查看node&#xff08;查询集群状态&#xff09; kubectl get no状态为kubec Ready 可以查看更多信息&#xff1a;-owide kubectl node -owide node没有命名空间隔离&#xff0c;所以node不需要指定命名空间 此处是一个master节点、两个worker节点、状态…

Neo4j:图数据库的革命性力量

Neo4j 首席技术官 prathle 撰写了一篇出色的博文&#xff0c;总结最近围绕 GraphRAG 的热议、我们从一年来帮助用户使用知识图谱 LLM 构建系统中学到的东西&#xff0c;以及我们认为该领域的发展方向。Neo4j一时间又大火起来&#xff0c;本文将带你快速入门这神奇的数据库。 前…