八、基础 | Linux设备驱动

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

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

文中有些地方没贴出相应的函数原型,请自行查阅,或者用SouceInsight搜索自己的内核源码树(本人就是用该方式查阅函数的使用)

简单设备驱动开发基础知识,暂不考虑驱动框架。文章根据GFM排版https://github.com/TongxinV

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

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

bootcmd和bootargs

1.设置bootcmd使开发板能够通过tftp下载自己建立的内核源码树编译得到的zImage
  set 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进入到内核源码树下借用内核源码的体系来完成模块的编译链接的。

[TOC]

字符设备基础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_INFO
        return -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;
}

图片5

字符设备基础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 	0xe0200244

unsigned 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--> kmalloc
    cdev_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_region
    cdev_alloc
    cdev_add

register_chrdev_region/alloc_chrdev_region

register_chrdev_region
    __register_chrdev_region
    
alloc_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_register
		    kset_register
			    kobject_uevent


device_create
    device_create_vargs
        kobject_set_name_vargs
        device_register
            device_initialize
            device_add
                kobject_add
                device_create_file
                device_create_sys_dev_entry
                devtmpfs_create_node
                device_add_class_symlinks
                device_add_attrs
                device_pm_add
                kobject_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)的方式显然是不合适的,需要进行比较大的修改。我们要怎样才能够使他能够具有很强的移植性呢?——内核已经帮我们想好了办法,即内核提供访问寄存器的读写接口(函数),使用这些函数具有可移植性。其实现的原理就是用条件编译,如下比较:

blog007

代码示例(静态映射):

...
#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

[TOC]

随笔--Linux驱动框架入门之LED

驱动框架也用模块这种'机制'实现,为了在不需要的时候也能够卸载

##何谓驱动框架

什么是驱动框架,为什么需要驱动框架,基于驱动框架写驱动有什么优势

驱动编程协作要求:(1)接口标准化;(2)尽量降低驱动开发者难度

什么是驱动框架:标准化的驱动实现``统一管控系统资源,维护系统稳定

(1)内核中驱动部分维护者针对每个种类的驱动设计一套成熟的、标准的、典型的驱动实现,并把不同厂家的同类硬件驱动中相同的部分抽出来自己实现好,再把不同部分留出接口给具体的驱动开发工程师来实现,这就叫驱动框架。

譬如LED 亮灭肯定都会有,这就是同类硬件的相同部分,用内核开发工程师开发的这套成熟的、标准的、典型的驱动去实现。
A厂家的LED能调亮度,B厂家的LED就只有亮灭,那A厂家的调亮度就是同类硬件的不同部分,驱动工程师就要在基本的驱动实现上去添加

(2)内核维护者在内核中设计了一些统一管控系统资源的体系,这些体系让内核能够对资源在各个驱动之间的使用统一协调和分配,保证整个内核的稳定健康运行。譬如系统中所有的GPIO就属于系统资源,每个驱动模块如果要使用某个GPIO就要先调用特殊的接口先申请,申请到后使用,使用完后要释放。又譬如中断号也是一种资源,驱动在使用前也必须去申请。其他比如框架中的设备锁等。这些也是驱动框架的组成部分

(3)一些特定的接口函数、一些特定的数据结构,这些是驱动框架的直接表现。

驱动框架这个概念单靠文字说明很难理解,应在实际驱动编程中去体会上面的这几点

##LED驱动框架分析 ###内核驱动框架中LED的基本情况

开始一件事,不盲目,简单分析后再去分析源码。像分析uboot和kernel先看地图(Makefile)那样

相关文件:
(1)drivers/leds目录,这个目录就是驱动框架规定的LED这种硬件的驱动应该待的地方。
(2)led-class.c和led-core.c,这两个文件加起来属于LED驱动框架的第一部分,这两个文件是内核开发者提供的,他们描述的是内核中所有厂家的不同LED硬件的相同部分的逻辑。必要的需要花时间的就是内核提供的led-class.c和led-core.c文件。
(3)leds-xxxx.c,这个文件是LED驱动框架的第2部分,是由不同厂商的驱动工程师编写添加的,厂商驱动工程师结合自己公司的硬件的不同情况来对LED进行操作,使用第一部分提供的接口来和驱动框架进行交互,最终实现驱动的功能。

我自己学习使用的开发板是九鼎厂商生产的s5pv210,内核源码树为其提供的linux+qt的kernel
其(九鼎移植的内核)led驱动没有使用内核推荐的led驱动框架,文件放在放在drivers/char/led/x210-led.c

案例分析驱动框架的使用:
(1)以leds-s3c24xx.c为例。leds-s3c24xx.c中通过调用led_classdev_register来完成LED驱动的注册,而led_classdev_register是在drivers/leds/led-class.c中定义的。所以其实SoC厂商的驱动工程师是调用内核开发者在驱动框架中提供的接口来实现自己的驱动的。
(2)驱动框架的关键点就是:分清楚内核开发者提供了什么,驱动开发者自己要提供什么

典型的驱动开发行业现状:
(1)内核开发者对驱动框架进行开发和维护、升级,对应led-class.c和led-core.c
(2)SoC厂商的驱动工程师对设备驱动源码进行编写、调试,提供参考版本,对应leds-s3c24xx.c
(3)做产品的厂商的驱动工程师以SoC厂商提供的驱动源码为基础,来做移植和调试

###LED驱动框架源码

涉及到的文件:led-core.c和led-class.c

经过基本分析经过基本分析,发现LED驱动框架中内核开发者实现的部分主要是led-class.c。
led-class.c就是一个内核模块,对led-class.c分析应该从下往上,遵从对模块的基本分析方法。
为什么LED驱动框架中内核开发者实现的部分要实现成一个模块?因为内核开发者希望这个驱动框架是可以被装载/卸载的。

blog009

subsys_initcall: 点击这里:linux内核段属性机制(以subsys_initcall和module_init为例)

leds_init
blog010

类的创建。leds_init除了对class结构体变量进行赋值,还做了class_create,所以到开发板下 ls /sys/class可以看到多了一个类leds,但是里面是空的。里面的用来与应用层进行交互的文件是驱动程序要去创建的,这就是驱动工程师要做的(用提供的接口去创建)

led_class_attrs:
(1)attribute是什么,对应将来/sys/class/leds/xxx目录里的内容,一般是文件和文件夹。这些文件其实就是sysfs开放给应用层的一些操作接口(非常类似于/dev/目录下的那些设备文件)
(2)attribute有什么用,作用就是让应用程序可以通过/sys/class/leds/xxx目录下面的属性文件来操作驱动进而操作硬件设备。
(3)attribute其实是另一条驱动实现的路线。有区别于之前讲的file_operations那条线。不同设备驱动实现走的路不一定,像这边的led走的是attribute,而LCD走的是file_operations

leds_class(class结构体):
以下文件在/include/linux/device.h

struct class {
	const char		*name;
	struct module		*owner;

	struct class_attribute		*class_attrs;
	struct device_attribute		*dev_attrs;
	struct kobject			*dev_kobj;

	int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
	char *(*devnode)(struct device *dev, mode_t *mode);

	void (*class_release)(struct class *class);
	void (*dev_release)(struct device *dev);

	int (*suspend)(struct device *dev, pm_message_t state);
	int (*resume)(struct device *dev);

	const struct kobj_ns_type_operations *ns_type;
	const void *(*namespace)(struct device *dev);

	const struct dev_pm_ops *pm;

	struct class_private *p;
};

唤醒(resume)、挂起(suspend):

led_classdev_register
blog011

属于某一类的设备的创建。分析可知,led_classdev_register这个函数其实就是去创建一个属于leds这个类的一个设备。其实就是去注册一个设备。所以这个函数其实就是led驱动框架中内核开发者提供给SoC厂家驱动开发者的一个注册驱动的接口
当使用led驱动框架去编写驱动的时候,这个led_classdev_register函数的作用类似于驱动程序使用file_operations方式去注册字符设备驱动时的register_chrdev函数。

led_classdev:
以下文件在/include/linux/leds.h

struct led_classdev {
	const char		*name;
	int			 brightness;
	int			 max_brightness;
	int			 flags;
	...
	/* Set LED brightness level */
	/* Must not sleep, use a workqueue if needed */
	void		(*brightness_set)(struct led_classdev *led_cdev,
					  enum led_brightness brightness);
	/* Get LED brightness level */
	enum led_brightness (*brightness_get)(struct led_classdev *led_cdev);
	...
	struct device		*dev;
	struct list_head	 node;			/* LED Device list */
	const char		*default_trigger;	/* Trigger to use */

    ...
};

##基于驱动框架写LED驱动

实践之前需要确认内核是否添加led驱动框架支持(通过make menuconfig配置,设置成功后到开发板下ls /sys/class可以看到多了一个类leds 但是里面是空的)

一般不从零开始写,参考哪里? drivers/leds/leds-s3c24xx.c;
思路/关键点:led_classdev_register,我们之前写的led驱动,直接看的是内核里面提供的操作接口,譬如register_chardev,cdev结构体等那些
仍然是驱动模块,module_init(XXX),insmod时会去调用XXX去注册,在xxx模块安装函数中去使用专用的接口

注:以前看注册成功与否,cat     /proc/devices去看信息变化。现在要看/sys/class/leds 如果注册成功该目录下会多一些文件

示例程序leds-s5pv210.c

图片.5-2-05

现象: 第1:写的驱动能够工作了,被加载了,/sys/class/leds/目录下多出来了一个表示设备的文件夹。文件夹里面有相应的操控led硬件的2个属性brightness和max_brightness
第2:led-class.c中brightness方法有一个show方法和store方法,这两个方法对应用户在/sys/class/leds/myled/brightness目录下直接去读写这个文件时实际执行的代码。
show brightness ...时,实际就会执行led_brightness_show函数
echo 1 > brightness时,实际就会执行led_brightness_store函数

show方法实际要做的就是读取LED硬件信息,然后把硬件信息返回给用户即可。所以show方法和store方法必须能够操控硬件。但led-class.c文件属于驱动框架中的文件,本身无法直接读取具体硬件,因此在show和store方法中使用函数指针的方式调用了struct led_classdev结构体中的相应的读取/写入硬件信息的方法(在linux/leds.h中,写驱动就一定要包含这个头文件和对应的led_classdev结构体类型变量。当有多个个同类设备(即多个led_classdev结构体类型变量)它是怎么判段的呢?--不难,文章不想写太长,自行分析)(算了还是写一下吧,都整理出来了)。
struct led_classdev结构体中的实际用来读写硬件信息的函数,就是驱动文件leds-s5pv210.c中要提供的(如示例程序)。

分析echo 1 > b brightness 是如何传递的:
p.5-2-06

##在硬件操作上驱动只应该提供机制而不是策略

在驱动中将4个LED分开,使应用层可以完全按照自己的需要对LED进行控制

好处:驱动层实现对各个LED设备的独立访问,并向应用层展示出4个操作接口led1、led2、led3、led4,这样应用层可以完全按照自己的需要对LED进行控制。

驱动的设计理念:不要对最终需求功能进行假定,而应该只是直接的对硬件的操作。有一个概念就是:机制和策略的问题。在硬件操作上驱动只应该提供机制而不是策略。策略由应用程序来做。比如有人要造反,提供枪支弹药这就是机制;比如马克思主义,没有枪支弹药,但是有策略

将4个LED分开程序: leds-s5pv210.c

##总结:LED驱动开发

不是很复杂的一个框架,多分析思考;关键点:细节另外再说,首先要明白写的驱动如何与框架结合(即应用到硬件是怎样的一条路线)

p.5-2-07

谈论attribute方式的驱动实现路线

分析LED驱动框架源码的时候,我们知道了led的一种驱动实现方式--attribute路线。源码中没有register_chrdev,只有class_create和device_create。通过对register_chrdev代码实现的分析,我们知道有register_chrdev一定走的是file_operations路线详细点击这里
所以猜测attribute路线是一条不依赖于内核维护的字符设备数组的驱动实现方式,虽然它需要设备号,但是这个设备号是没有意义的。

因为我通过生成的设备文件找到内核维护的字符设备数组的某个位置后没办法再往下找了(因为你没有调用register_chrdev,即没有像调用register_chrdev时会产生的char_device_struct结构体变量)。

实际测试发现驱动模块leds-s5pv210模块安装后,lsmod控制台打印出相应的模块安装信息,但是/dev下并没有产生相应的设备节点。进一步思考,device_create本质是提供相应信息给udev,让udev在用户空间下去创建设备节点,没有则说明了需要真正的主设备的存在

随笔--linux内核的gpiolib学习

gpiolib引入:(1)一个事实:很多硬件都要用到GPIO,GPIO会复用;(2)如果同一个GPIO被2个驱动同时控制了,就会出现bug;(3)内核提供gpiolib来统一管理系统中所有GPIO;(4)gpiolib本身属于驱动框架的一部分

和动态映射静态映射读写寄存器比较起来

gpiolib学习重点

  • gpiolib的建立过程:和虚拟地址的建立类似,内核在启动的过程中去调用gpiolib建立函数建立gpiolib
  • gpiolib的使用方法:申请、使用、释放
  • gpiolib的架构:涉及哪些目录的哪些文件里面的哪些函数哪些数据结构哪些变量,谁调用谁,为什么要分开为什么不写在一个文件,等能理解这些的时候就有了架构意识。继续努力!

[TOC]

开机调用gpiolib的建立函数

gpiolib的建立函数:s5pv210_gpiolib_init

p.5-3-01

gpiolib的建立

填充一个个s3c_gpio_chip这个结构体定义的变量(GPIO端口的抽象),最后把一个个端口的抽象的地址挂接到内核维护的一个gpio_desc结构体指针数组

p.5-3-02

s3c_gpio_chip结构体

(1)这个结构体是一个GPIO端口的抽象,这个结构体的一个变量就可以完全的描述一个端口。
(2)端口IO口是两个概念。S5PV210有很多个IO口(160个左右),这些IO口被分成N个端口(port),每个端口包含了M个IO口。(M个IO口又被映射到一个一个的寄存器).(有时候一个端口就只有一个寄存器代表有时候也有多个)。譬如GPA0是一个端口组,里面包含了8个IO口,我们一般记作:GPA0_0(或GPA0.0)、GPA0_1...
这是芯片设计决定的
(3)内核中为每个GPIO分配了一个编号,编号是一个数字(譬如一共有160个IO时编号就可以从1到160连续分布),编号可以让程序很方便的去识别每一个GPIO。

附:
arch/arm/plat-samsung/include/plat/gpio-core.h:struct s3c_gpio_chip
include/asm-generic/gpio.h:struct gpio_chip
arch/arm/plat-samsung/include/plat/gpio-cfg.h:struct s3c_gpio_cfg

s5pv210_gpio_4bit结构体数组

(1)这个东西是一个结构体数组,数组中包含了很多个struct s3c_gpio_chip类型的变量。一个struct s3c_gpio_chip类型的变量就代表了一个端口
(2)S5PV210_GPA0宏:设置当前端口的基础编号(读者自行追踪,慢慢体会),区别s3c_gpio_chip里的base
附:S5PV210_GPA0宏

函数samsung_gpiolib_add_4bit_chips

具体进行gpiolib的注册的。这个函数接收的参数是当前文件中定义好的结构体数组s5pv210_gpio_4bit(2个参数分别是数组名和数组元素个数),这个数组中其实就包含了当前系统中所有的IO端口的信息(这些信息包含:端口的名字、端口中所有GPIO的编号、端口操作寄存器组的虚拟地址基地址、端口中IO口的数量、端口上下拉等模式的配置函数、端口中的IO口换算其对应的中断号的函数等,有一些当前还没被赋值,接下来会被填充好)

samsung_gpiolib_add_4bit_chips
    ├── samsung_gpiolib_add_4bit    填充每一个GPIO被设置成输入模式/输出模式的操作方法
    └── s3c_gpiolib_add             检测并完善chip的direction_input/direction_ouput/set/get这4个方法
            │
            └── gpiochip_add        真正向内核注册gpiolib的函数
            
                        这个注册就是将我们的封装了一个GPIO端口的所有信息的chip结构体变量挂接到
                        内核gpiolib模块定义的一个gpio_desc数组中的某一个格子中
                        (和前面两个不一样,这个不是三星工程师写的,这个是内核开发者写的;
                          驱动就是内核开发者写一部分,厂商驱动开发工程师写一部分)

arch/arm/plat-samsung/gpiolib.c:samsung_gpiolib_add_4bit
arch/arm/plat-samsung/gpio.c:s3c_gpiolib_add
drivers/gpio/gpiolib.c:gpiochip_add

(1)哪个目录的哪个文件
(2)函数名中为什么有个4bit:三星的CPU中2440的CON寄存器是2bit对应一个IO口,而6410和210以及之后的系列中CON寄存器是4bit对应1个IO口。所以gpiolib在操作2440和210的CON寄存器时是不同的

整体框架

到这里停下来看看,到这里gpiolib的建立已经分析完了。不考虑数据结构s3c_gpio_chip等这些东西定义对应的目录,整体观望分析的目录和文件结构

p.5-3-07

从驱动框架角度分析gpiolib

之前的分析已经告一段落,目前已经搞清楚了gpiolib的建立工程。但是这只是整个gpiolib建立的一部分,是厂商驱动工程师负责的那一部分;还有另一部分是内核开发者提供的驱动框架的那一部分,就是接下来要去分析的第2部分 drivers/gpio/gpiolib.c这个文件中所有的函数构成了第2部分,也就是内核开发者写的gpiolib框架部分

drivers/gpio/gpiolib.c概览

drivers/gpio/gpiolib.c这个文件中提供的函数主要有以下部分:

gpiochip_add: 是框架开出来的接口,给厂商驱动工程师用,用于向内核注册我们的gpiolib
gpio_request: 是框架开出来的接口,给使用gpiolib来编写自己的驱动的驱动工程师用的,
              驱动中要想使用某一个gpio,就必须先调用gpio_request接口来向内核的gpiolib申请资源,得到允许后才可以去使用这个gpio
gpio_free:	  对应gpio_request,用来释放申请后用完了的gpio
gpio_request_one/gpio_request_array: 这两个是gpio_request的变种
gpiochip_is_requested:	接口用来判断某一个gpio是否已经被申请了
gpio_set_value设置输出值  gpio_get_value获取IO口值
gpio_direction_input/gpio_direction_output: 接口用来设置GPIO为输入/输出模式

以上的接口属于一类,这些都是给写其他驱动并且用到了gpiolib的人使用的
剩下的还有另外一类函数,是gpiolib内部自己的一些功能实现的代码

gpio_direction_input/gpio_direction_output:
p.5-3-08

gpiolib的attribute部分

和之前文章提到的LED驱动框架相比较,分析gpiolib的建立过程,整个过程其实就是在填充一个结构体变量,然后绑定到一个description数组。但是还有一个问题就是/sys/class/gpio这个文件是怎么来的,/sys/class/gpio底下的那些设备文件又是怎么来的,之前的led驱动是通过class_createdevice_create来创建设备文件。所以可以推测gpiolib也是用这两个函数

gpiolib驱动也是用模块的机制实现的,从下往上分析:

p.5-3-091

DEVICE_ATTR相关知识:点击这里

使用gpiolib完成led驱动

第1步:使用gpio_request申请要使用的一个GPIO
第2步:gpio_direction_input/gpio_direction_output 设置输入/输出模式
第3步:设置输出值---gpio_set_value;获取IO口值---gpio_get_value

示例程序:leds-s5pv210.c 图中高亮部分为新添加 ;源代码

上述程序只是在led1上编写代码测试,扩展支持led2和led3、led4.可以分开注册也可以使用gpio_request_array去一次性注册

学习linux中查看gpio使用情况的方法:
内核中提供了虚拟文件系统debugfs,里面有一个gpio文件,提供了gpio的使用信息。 使用方法:mount -t debugfs debugfs /tmp,然后cat /tmp/gpio即可得到gpio的所有信息,使用完后umount /tmp卸载掉debugfs

总结:gpiolib工作原理

p.5-3-093

将驱动添加到内核中

点击这里

随笔--linux设备驱动模型

kobject为底层,组织类class总线bus设备device驱动driver等高级数据结构,同时实现对象引用计数、维护对象链表、对象上锁、对用户空间的表示等服务

参考博客:
Linux设备驱动模型http://blog.csdn.net/xiahouzuoxin/article/details/8943863
Linux设备模型--设备驱动模型和sysfs文件系统解读http://www.cnblogs.com/Ph-one/p/5052191.html

[TOC]

linux设备驱动模型简介

站在设备驱动这个角度分析,设备驱动模型是如何构建出来,起到什么作用,认识它并在写驱动的时候去利用设备驱动模型

####什么是设备驱动模型

从以下几个角度去描述折别驱动模型

  1. 类class、总线bus、设备device、驱动driver

     四个词并非四个简单概念,还是linux驱动的四个结构体,也可以理解为linux设备驱动模型的四个框架。
     分别对应我们源代码的四个结构体。
         class :例如class_create
         bus   :例如USB总线,SPI总线
         device:设备
         driver:驱动
     为何需要这四个结构体-----生产这些结构体类型的变量,每一个结构体变量就代表一个实例
    
  2. kobject和对象生命周期的管理

     linux内核源代码里面的一个结构体,k是kernel object物体,kobject就是内核的一个东西。一个高度抽象的结构体,表示内
     核里面的一个对象,就是内核里面的所有对象抽象出来的一个总类。所以说linux内核是面向对象编程。这个kobject就类似于面
     向对象体系的一个总的基类,总的父类
     
     对象生命周期,例如某一个驱动,insmod诞生,rmmod消亡。如何管理这个对象的生命周期?在kobject有一种机制,能够让每一个
     对象具有自我管理生命周期的特性,即自己管理自己,当自身不被需要的时候就自己释放。比如为A malloc 申请一段内存,当不需
     要的时候不用去free A,它自己会自己free掉。如何知道自己没用了呢?肯定需要一种方法
    
  3. sysfs

     在内核空间和我们用户空间建立一个映射关系
     
    
  4. udev

     为了实现内核空间和用户空间的信息的一个同步
     
    

####为什么需要设备驱动模型

(1)早期内核(2.4之前)没有统一的设备驱动模型,但照样可以用
(2)2.6版本中正式引入设备驱动模型,目的是在设备越来越多,功耗要求等新特性要求的情况下让驱动体系更易用、更优秀
(3)设备驱动模型负责统一实现和维护一些特性,诸如:电源管理、热插拔、对象生命周期、用户空间和驱动空间的交互等基础设施
(4)设备驱动模型目的是简化驱动程序编写,但是客观上设备驱动模型本身设计和实现很复杂。主要学会如何使用设备驱动模型

驱动开发的2个点:

(1)驱动源码本身编写、调试。重点在于对硬件的了解。
(2)驱动什么时候被安装(被安装就是insmod,而且insmod方式是自动的,比如一个触摸屏的芯片已经装上,开机后就能够自动装上,
然后一拔掉这个驱动就自己卸载)、驱动中的函数什么时候被调用(即在应用层怎么做就能调用驱动写好的东西)。这些跟硬件无关,完
全和设备驱动模型有关

设备驱动模型的底层架构

####kobject p.5-3-14

(1)定义在linux/kobject.h中
(2)各种对象最基本单元,提供一些公用型服务如:对象引用计数、维护对象链表、对象上锁、对用户空间的表示
(3)设备驱动模型中的各种对象其内部都会包含一个kobject
(4)地位相当于面向对象体系架构中的总基类

####kobj_type p.5-3-12

(1)很多书中简称为ktype,每一个kobject都需要绑定一个ktype来提供相应功能
                                               (绑定和包含意思不一样,包含是实体,绑定是实体的一个指针)
(2)关键点0:release,释放。用release而不用close,因为可能需要反复去打开,第一次打开第二次打开就打开了两次,如果
用close就一次把两个都关掉了。release不一样,当去release时,会先判断有没有被其他人打开过,如果被其他人打开着,那
么就只是减少它的一次引用计数;如果没有被其他人打开,当前就只有我打开它,那关闭时就彻底的把它的东西全部释放掉

(3)关键点1:sysfs_ops,提供该对象在sysfs中的操作方法(show和store)
struct sysfs_ops {
    ssize_t (*show )(struct kobject *, struct attribute *,char *);
    ssize_t (*store)(struct kobject *,struct attribute *,const char *, size_t);
};
show方法用于将传入的指定属性编码后放到char *类型的buffer中
store则执行相反功能:将buffer中的编码信息解码后传递给struct attribute类型变量。两者都是返回实际的属性长度

(4)关键点2:attribute,提供在sysfs中以文件形式存在的属性,其实就是应用接口
struct attribute {
    const char *name;/* 属性名称 */
    mode_t mode;     /* 属性保护:只读设为S_IRUGO,可写设为S_IWUSR */
}

####kset p.5-3-13

(1)kset的主要作用是做顶层kobject的容器类,用来这种上下文的
(2)kset的主要目的是将各个kobject(代表着各个对象)组织出目录层次架构
(3)可以认为kset就是为了在sysfs中弄出目录,从而让设备驱动模型中的多个对象能够有层次有逻辑性的组织在一起

可以理解为上面三个底层架构是为了实现/sys目录下的那些东西。kobject提供的是一个最基本的功能,是构成别的的一个基础;kobj_type是提供目录底下的那些文件以及对文件操作的方法;kset是用来构建目录层次架构。这三个结合起来就提供了 [/sys目录下的那些东西被实现] 的基础架构

总线式设备驱动组织方式

总线是在 [设备驱动模型的以kobject为代表的底层]之上的层次,可以把它看作是中间层,实际上字符设备驱动模型的层次不止三个层,可能有五个六个层次。层次越往上,越靠近实际的编程,越下的话越靠近写内核的那些人

####总线(struct bus_type) p.5-3-15

p.5-3-16

(1)物理上的真实总线及其作用(英文bus)
(2)驱动框架中的总线式设计
(3)bus_type结构体,关键是match函数和uevent函数

总线的bus_type结构体就是一种抽象,抽象出了总线里面所有需要管理的事情所有需要具备的功能。将来去内核构造一个一个总线的时候就是产生bus_type这个结构体的一个结构体变量,然后把它适当的填充

####设备(struct device) p.5-3-17

(1)struct device是硬件设备在内核驱动框架中的抽象
(2)device_register用于向内核驱动框架注册一个设备
(3)通常device不会单独使用,而是被包含在一个具体设备结构体中,如struct usb_device

####驱动(struct device_driver) p.5-3-18

(1)struct device_driver是驱动程序在内核驱动框架中的抽象
(2)关键元素1:name,驱动程序的名字,很重要,经常被用来作为驱动和设备的匹配依据
(3)关键元素2:probe,驱动程序的探测函数,用来检测一个设备是否可以被该驱动所管理。

probe函数对于一个驱动函数非常重要,有些简单的驱动没有探测函数,因为没有使用到linux内核的驱动框架,没有用到总线式
驱动框架来写。即非总线式的写法是不需要probe函数的,probe函数是总线式的框架才需要的。在总线方式下probe函数就是驱
动最重要的函数。

官方对它的解释是检测一个设备是否可以被该驱动所管理。意思就是当装载了一个设备,驱动中的probe函数就会去看它和装载的这个设备匹配不匹配。probe是驱动程序的入口

总线、设备、驱动是一组的,只有总线没有意义,只有设备、驱动则工作不起来

###类(class) (1)相关结构体:struct class 和 struct class_device 前者是xx类,后者是xx类下面的某一个设备
(2)udev的使用离不开class 像基础篇的设备类操作class_create,device_create
(3)class的真正意义在于作为同属于一个class的多个设备的容器。也就是说,class是一种人造概念,目的就是为了对各种设备进行分类管理。当然,class在分类的同时还对每个类贴上了一些“标签”,这也是设备驱动模型为实际写驱动提供的基础设施

class的真正意义在于作为同属于一个class的多个设备的容器,其实类被发明出来就是用来管理设备的。上面讲的总线也是
用来管理设备的,设备这个东西接受多重管理。一方面某一个设备从属于某一总线,另一方面某一个设备也从属于某一个类

注:从目录关系也能体现总线和类的管理方式,/sys/devices 放的是真正的设备相关的东西,你从class目录进去或者从bus目录进去最后都会由函数指针指向devices这个目录里面来

驱动这个东西并不复杂,就是由上面提到的这些理论来指导,由实践来构成

随笔--platform平台总线

何为平台总线--为了方便管理扩展到CPU32位地址空间的设备

总线只是一种机制,一种管理方式,真正的起关键作用的是LED驱动框架

关键点:

  • platform平台总线的目的、意义
  • platform自己本身的构建和工作原理
  • 如何使用平台总线来组织设备和驱动

platform平台总线介绍

####何为平台总线 (1)相对于usb、pci、i2c等物理总线来说,platform总线是虚拟的、抽象出来的
(2)CPU与外部通信的2种方式:地址总线式连接(LED 串口 DM9000等可以直接通过寄存器操控)和专用接口式连接(USB Nand等)。 平台总线对应地址总线式连接设备

专用接口其实就是对应usb、pci、i2c等物理总线,就是通过这些物理总线跟外部连接的。那么扩展到CPU32位地址空间的设备
怎么办?扩展到CPU32位地址空间的设备是不对应具体的一种的这种物理总线,所以我们就给他发明了平台总线

思考:为什么要有平台总线?总线其实就是为了USB、I2C、PCI这些设备设计的,扩展到CPU32位地址空间的设备其实是没有总线这种概念的。但是为了避免有一部分设备有总线有一部分设备没有总线的管理不便,所以干脆一罐子全部打倒,给所有的设备都规定都有总线。平台总线的发明其实就是为了给那些本来不需要总线的设备来连接

####平台总线下的两个主要结构体 p.5-3-191 p.5-3-192

(1)platform工作体系都定义在drivers/base/platform.c
(2)两个结构体:platform_deviceplatform_driver
(3)两个接口函数:platform_device_registerplatform_driver_register

platform平台总线工作原理

第一、系统启动时在bus系统中注册platform,主要内容是platform_bus_type中的match方法的注册;
第二、我们注册写好的设备和驱动;
第三、platform的match函数发现driver和device匹配后,调用driver的probe函数来完成驱动的初始化和安装

####启动时注册platform总线

platform总线的注册是由platform_bus_init函数完成的,主要的内容是注册一个bus_type结构体类型的变量

![p.5-3-193)

详细分析点击这里系统启动时在bus系统中注册platform

(1)每种总线(不光是platform,usb、i2c那些也是)都会带一个match方法,match方法用来对总线下的device和driver进行匹配

理论上每种总线的匹配算法是不同的,但是实际上一般都是看name的

(2)platform_match函数就是平台总线的匹配方法。该函数的工作方法是:如果有id_table就说明驱动可能支持多个设备。所以这时候要去对比id_table中所有的name,只要找到一个相同的就匹配上了不再找了,如果找完id_table都还没找到就说明每匹配上;如果没有id_table或者每匹配上,那就直接对比device和driver的name,如果匹配上就匹配上了,如果还没匹配上那就匹配失败

可能先有驱动再有设备,也有可能先有设备再装的驱动

####platform设备和驱动的注册过程

以led为例

两个主要结构体:
(1)platform_device
(2)platform_driver

两个主要函数:
(1)platform_device_register
(2)platform_driver_register

platform设备注册过程

p.5-3-194

(1)platdata:平台设备的特有平台数据,其实就是设备注册时提供的设备有关的一些数据(譬如设备对应的gpio、使用到的中断号、设备名称等)
(2)这些数据在设备和驱动match之后,会由设备方转给驱动方。驱动拿到这些数据后,通过这些数据得知设备的具体信息,然后来操作设备
(3)这样做的好处是:驱动源码中不携带数据,只负责算法(对硬件的操作方法)。之前写的驱动可以单独运行是因为单独的驱动就包含了硬件信息,这是不适宜的。现代驱动设计理念就是算法和数据分离,这样最大程度保持驱动的独立性和适应性

platform驱动注册过程

到代码实践环节中再分析

(1)match函数
(2)probe函数

###总结(1)

  • 从意义上看,platform总线的发明为了便于管理扩展到CPU32位地址空间的设备,也为了能够统一管理系统中的所有设备(总线其实就是为了USB、I2C、PCI这些设备设计的,扩展到CPU32位地址空间的设备其实是没有总线这种概念的)
  • 从代码层次上看,platform总线是以kset和kobject为代表的设备驱动模型底层构建起来的的上层,对应的结构体为platform_bus_type,platform_device和platform_driver
  • 如何实现:platform平台总线工作原理

###platform平台总线代码实践

照猫画虎,以自带的leds-s3c24xx.c为猫

添加led设备和平台设备特有平台数据
根据前面platform设备注册过程的分析添加设备抽象

分析并移植leds-s3c24xx.c(即led驱动):
程序框架:

暂略,也可以理解了之后自己回顾

leds-s5pv210.c

p.5-3-195

s5pv210_led_probe:

p.5-3-196

###总结(2)

  • 写驱动需要熟悉设备驱动模型,熟悉各种数据结构的成员变量和成员方法的意义,以及各种东西之间的关联。还有就是如果接触到一种新的驱动,要能够分析源代码找到要编写的这个驱动所涉及到的驱动框架是怎么样的,这是需要具备的能力
  • 理解并使用数据和算法分离的思想
  • 总线这种方式虽然重要,但是它只是一种机制,一种管理方式。真正的起关键作用的是LED驱动框架(就是内核实现一部分,驱动实现一部分)

随笔--MISC类设备驱动

misc类设备的本质是字符设备,在驱动框架中使用register_chrdev注册了一个主设备号为10的设备

[TOC]

misc类设备介绍

  1. 何为misc

(1)中文名:杂散设备
(2)/sys/class/misc
(3)典型的字符设备。misc是对原始的字符设备注册接口的一个类层次的封装,很多典型字符设备都可以归类到misc类中,使用misc驱动框架来管理
(4)像LED一样也有一套驱动框架,内核实现一部分(misc.c,led是led-class.c),驱动实现一部分(x210-buzzer.c)

  1. 涉及文件

/drivers/char/misc.c
/driver/char/buzzer/x210-buzzer.c

misc驱动框架源码分析

源码框架的主要工作:注册misc类,使用老接口注册字符设备(主设备号10),开放device注册的接口misc_register给驱动工程师

####驱动框架模块的注册

在框架中使用register_chrdev注册了一个主设备号为10的设备,而在设备驱动中device_create创建设备文件主设备号都为10,次设备号不同,从而实现分类(分为misc类)。我们也可以模仿这种方式创建自己的分类,但是其实不简单,还有好多东西需要积累!

以下文件位于/drivers/char/misc.c:

pic.5-5-01

misc_open:分析misc_open,发现其最终指向某一个设备对应的miscdevice结构体变量中的file_operations结构体变量中的open。(没具体分析应用层打开misc类设备是通过这里的open间接调用具体的open,还是直接调用某一具体misc设备的open)

####开放出来的注册接口

misc_register

以下文件位于/drivers/char/misc.c:

pic.5-5-02

struct miscdevice:misc类设备的抽象

misc_list:内核维护的管理misc设备类的链表,将来添加一个设备就往这个链表添加相应的miscdevice的结构体变量的地址(指针)

misc设备驱动源码分析

####以misc类设备蜂鸣器为例

定义一个struct miscdevice结构体变量并进行填充,然后调用驱动框架提供的misc_register进行注册。

下列代码位于/driver/char/buzzer/x210-buzzer.c:

pic.5-5-03

miscdevice

pic.5-5-04

x210_pwm_ioctl

pic.5-5-05 pic.5-5-06

x210_pwm_open/x210_pwm_close

pic.5-5-07

static struct semaphore lock/down_trylock(&lock)/up(&lock):信号量使用旧式接口,关于信号量的使用可以看内核源码如何使用

####板载蜂鸣器驱动测试

确认/dev目录下有buzzer设备,没有需要make menuconfig 进行配置(确保子Makefile和子Kconfig相关变量相一致)

简单测试app_buzzer.c:

pic.5-5-09

注意0:关于文中涉及到的内核链表可以参考这篇文章《内核链表实现分析与使用(双向环形链表)》

注意1:register_chrdev和device_creator的区别。register_chrdev是向内核维护的一个指针数组添加一个字符设备抽象对应的地址,来注册驱动;device_create是利用给的设备号和类名去创建设备文件(创建后的设备文件位于/dev)。

注意2:分析LED驱动框架源码的时候,并没有register_chrdev,只有class_create和device_creator,而且走的是attribute路线。所以用attribute方式实现的驱动只能通过/sys/class下的文件进行访问,详细可以查看这篇文章谈论attribute驱动实现方式(及device_create与设备节点的关系)

#嵌入式##面经#
Linux嵌入式必考必会 文章被收录于专栏

&quot;《Linux嵌入式必考必会》专栏,专为嵌入式开发者量身打造,聚焦Linux环境下嵌入式系统面试高频考点。涵盖基础架构、内核原理、驱动开发、系统优化等核心技能,实战案例与理论解析并重。助你快速掌握面试关键,提升竞争力。

全部评论
佬,图片都炸了
点赞 回复 分享
发布于 09-23 23:42 江苏

相关推荐

不愿透露姓名的神秘牛友
11-12 11:09
海尔 电气工程师 22W总包 硕士985
点赞 评论 收藏
分享
2 6 评论
分享
牛客网
牛客企业服务