1.Linux下编写EMMC驱动程序linuxemmc
2.linux设备驱动程序——i2c总线的设设备添加与实现
3.Linux内核-tty设备驱动程序
4.linux设备驱动程序——设备树(0)-dtb格式
Linux下编写EMMC驱动程序linuxemmc
Linux内核本身提供对eMMC存储器的支持,主要针对eMMC4.4的备驱支持;但有时候需要自行编写一个eMMC驱动程序;以下是一些如何编写Linux下的eMMC驱动程序的基本步骤:
1、首先需要自己熟悉eMMC标准以及了解相应的动程硬件相关信息;
2、然后,序源详解需要在Linux内核模块代码中定义一个合适的驱动数据结构用于保存eMMC硬件信息;
3、接下来,程序源码安装gnuradio需要在 Linux 内核模块中添加相应的设设备初始化函数用于硬件设备的初始化,用以设定硬件引脚,备驱使能eMMC控制器,动程wijf等;
4、序源详解添加eMMC的驱动核心函数,编写函数来处理eMMC的程序主要功能,比如读、设设备写、备驱读数据、动程写数据等;
5、最后,添加devic_driver函数用于调用上面定义的函数,并在这些函数中实现操作硬件所必须的部分代码;
6、在驱动程序中添加模块安装函数,用以把eMMC模块安装到Linux内核中:
/* 模块安装函数 */
static int __init eMMC_init(void)
{
int retval;
retval = register_blkdev(eMMC_MAJOR, eMMC_NAME);
if (retval
printk(KERN_WARNING “Unable to get eMMC major %d\n”, eMMC_MAJOR);
return retval;
}
dma_set_mask_and_coherent(NULL, DMA_BIT_MASK());
spin_lock_init(&eMMC_lock);
/* 初始化设备 */
if(eMMC_init_device() != 0)
return -ENOMEM;
/* 在 /proc/devices 中注册设备 */
if(eMMC_create_device() != 0)
return -ENOMEM;
return 0;
}
module_init(eMMC_init);
7、最后,编译驱动程序,并安装它到Linux内核中。
上述步骤说明了如何编写一个Linux下的eMMC驱动程序。在开发eMMC驱动程序时,需要根据硬件实际情况,对相应功能函数进行修改,并对相应的字段和标准进行熟悉,以确保写出一个功能完备、稳定可靠的eMMC驱动程序。
linux设备驱动程序——i2c总线的添加与实现
一文看懂linux内核详解
深入了解使用linux查看磁盘io使用情况
在实际驱动开发过程中,i2c总线也是集成在系统中的,驱动开发者不需要关心i2c总线的初始化,只需要将相应的i2c_client和i2c_driver挂载在总线上进行匹配即可。
那么,i2c总线在系统中是如何初始化得来的呢?
答案就在文件i2c-core-base.c中,它的ios 电商源码下载过程是这样的:
在i2c_init函数中,使用bus_register()将i2c总线注册到系统中,那么这个i2c_init()函数是在哪里被调用的呢?
在内核启动的过程中,进入C语言环境的第一个入口函数是start_kernel(),但是i2c_init()并非由start_kernel()间接调用,而是借助于linux内核中的initcall机制被调用,简而言之,就是将这个函数地址在编译阶段放入特定的段中,在内核初始化时由某个启动函数将这些段中的函数一一取出并执行。
i2c总线通过postcore_initcall()将init函数注册到系统中。
当模块被加载进系统时,就会执行i2c_init函数来进行初始化。
在i2c_bus_type结构体中,定义了match(),probe(),remove()和shutdown()函数,match()函数就是当有新的i2c_client或者i2c_driver添加进来时,试图寻找与新设备匹配的项,返回匹配的结果。
remove()和shutdown(),顾名思义,这两个函数是管理的驱动移除行为的。
对于probe函数,在之前的章节中提到:当相应的device和driver经由总线匹配上时,会调用driver部分提供的probe函数。
那么:
带着这两个疑问,我们接着往下看。
在这里我们不免要回顾一下前面章节所说的,作为一个驱动开发者,如果我们要开发某些基于i2c的设备驱动,需要实现的框架流程是怎样的:
但是问题是,为什么device和driver都注册进去之后,就会调用driver部分提供的probe函数呢?
为了解决这些问题,最好的办法就是看源代码,假设我们是以设备树的方式进行匹配,device(即i2c_client)部分已经被注册到系统中,此时我们向系统中注册(insmod由driver部分程序编译的模块)相应的driver部分,接下来我们跟踪driver部分注册到i2c总线的流程,看看它是怎么实现的:
跟踪i2c_add_driver:
经过一系列的代码跟踪,找到了bus_add_driver(),根据名称可以知道,幼儿园asp源码这个函数就是将当前i2c_driver添加到i2c_bus_type(即i2c总线)中。
接着往下看:
从第一部分可以看到,将当前drv链入到bus->p->klist_drivers链表中,那么可以猜到,在注册device部分的时候,就会将device链入到bus->p->klist_devices中。
然后,再看driver_attach(drv):
driver_attach调用bus_for_each_dev,传入当前驱动bus(即i2c_bus_type),当前驱动drv,以及一个函数__driver_attach。
bus_for_each_dev对每个device部分调用__driver_attach。
在__driver_attach中,对每个drv和dev调用driver_match_device(),并根据函数返回值决定是否继续执行调用driver_probe_device()。
从函数名来看,这两个函数大概就是我们在前文中提到的match和probe函数了,我们不妨再跟踪看看,先看看driver_match_device()函数:
果然不出所料,这个函数十分简单,如果当前驱动的所属的bus有相应的match函数,就调用match函数,否则返回1.
当前driver所属的总线为i2c_bus_type,根据上文i2c总线的初始化部分可以知道,i2c总线在初始化时提供了相应的match函数,所以,总线的match函数被调用,并返回匹配的结果。
接下来我们再看看driver_probe_device()函数:
在driver_probe_device中又调用了really_probe,在really_probe()中,先判断当前bus总线中是否注册probe()函数如果有注册,就调用总线probe函数,否则,就调用当前drv注册的probe()函数。
到这里,我们解决了上一节中的一个疑问:总线和driver部分都有probe函数时,程序是asp.net源码 csdn怎么处理的?
答案已经显而易见了:优先调用总线probe函数。
而且我们理清了总线的match()函数和probe()函数是如何被调用的。
那么,上一节中还有一个疑问没有解决:总线的match和probe函数执行了一些什么行为?
对于总线probe函数,获取匹配成功的device,再由device获取driver,优先调用driver->probe_new,因为driver中没有设置,直接调用driver的probe函数。
总线probe和driver的probe函数的关系就是:当match返回成功时,优先调用总线probe,总线probe完成一系列的初始化,再调用driver的probe函数,如果总线probe函数不存在,就直接调用driver的probe函数,所以当我们在系统中添加了相应的device和driver部分之后,driver的probe函数就会被调用。
对于总线match函数,我们直接查看在i2c总线初始化时的函数定义:
i2c_device_match就是i2c_driver与i2c_device匹配的部分,在i2c_device_match函数中,可以看到,match函数并不只是提供一种匹配方式:
接下来再深入函数内部,查看匹配的细节部分:
在i2c_of_match_device中,调用了of_match_device()和i2c_of_match_device_sysfs()两个函数,这两个函数代表了两种匹配方式,先来看看of_match_device:
of_match_device调用of_match_node。
of_match_node调用__of_match_node函数。
如果你对设备树有一定的了解,就知道系统在初始化时会将所有设备树子节点转换成由struct device_node描述的节点。
在被调用的__of_match_node函数中,对device_node中的compatible属性和driver部分的of_match_table中的compatible属性进行匹配,由于compatible属性可以设置多个,所以程序中会对其进行逐一匹配。
我们再回头来看设备树匹配方式中的i2c_of_match_device_sysfs()匹配方式:
由i2c_of_match_device_sysfs()的实现可以看出:当设备树中不存在设备节点时,driver部分的of_match_table中的compatible属性试图去匹配i2c_client(device部分)的.driver.name属性.
因为设备树的默认规则,compatible属性一般形式为"vender_id,product_id",当compatible全字符串匹配不成功时,取product_id部分再进行匹配,这一部分主要是java权限管理框架源码兼容之前的不支持设备树的版本。
acpi匹配方式较为少用且较为复杂,这里暂且不做详细讨论
id_table匹配方式中,这种匹配方式一目了然,就是对id_table(driver部分)中的.name属性和i2c_client(device部分)的.name属性进行匹配。那么i2c_client的.name是怎么来的呢?
在非设备树的匹配方式中,i2c_client的.name属性由驱动开发者指定,而在设备树中,i2c_client由系统对设备树进行解析而来,i2c_client的name属性为设备树compatible属性"vender_id,product_id"中的"product_id",所以,在进行匹配时,默认情况下并不会严格地要求 of_match_table中的compatible属性和设备树中compatible属性完全匹配,driver中.drv.name属性和.id.name属性也可以与设备树中的"product_id"进行匹配。
关于linux i2c总线的初始化以及运行机制的讨论就到此为止啦
Linux内核-tty设备驱动程序
在Linux系统中,终端设备是字符型设备,通常用tty来简称为各种类型的终端设备。tty源于Teletype,最早的一种终端设备,类似于电传打字机。Linux内核包含了以下几类终端设备:
1. 串行端口终端(/dev/ttySn):通过计算机串行端口连接的设备。每个串行端口被视为字符设备,如/dev/ttyS0或/dev/ttyS1,设备号为(4,0)或(4,1)。通过命令行将标准输出重定向到设备文件名上,即可通过端口发送数据,例如:echo test > /dev/ttyS1将“test”发送到连接在ttyS1端口的设备。
2. 伪终端(/dev/pty/):成对的逻辑终端设备,如/dev/ptyp3和/dev/ttyp3。它们与实际物理设备无直接关联。如果一个程序将ttyp3视为串行端口设备,其读/写操作在ptyp3上体现,而ptyp3则是另一个程序用于读写操作的逻辑设备。这样,两个程序可通过逻辑设备进行交流,使用ttyp3的程序认为自己与串行端口通信。
3. 控制台终端(/dev/ttyn, /dev/console):当当前进程有控制终端时,/dev/tty为控制终端的设备特殊文件。UNIX系统中,计算机显示器通常作为控制台终端,仿真一种名为Linux的终端(TERM=Linux),并关联有相关设备文件:tty0、tty1、tty2等。用户登录时使用tty1,通过Alt+[F1—F6]切换到tty2等。
Linux终端设备驱动的框架结构包含tty核心、tty线路规程和tty驱动。Linux终端设备的结构体tty_driver结构体是任何tty驱动的主要数据结构,用于注册和注销tty驱动到tty内核。tty_operations结构体包含与tty_driver同名成员函数的函数指针,用于定义操作行为。tty_struct结构体由tty核心用来保存当前tty端口的状态,大多数成员仅由tty核心使用。
Linux终端设备的装载和卸载涉及一组用于操作tty_driver结构体及tty设备的函数,包括注册和注销函数。注册tty设备使用tty_register_device()函数,参数为设备索引,例如:tty_register_device(0)。注销tty设备使用对应函数,如:tty_unregister_device()。分配和注册tty驱动使用tty_alloc_driver()和tty_register_driver()函数,分别用于初始化和注册tty_driver。设置tty驱动操作使用tty_set_operations()函数,分配tty驱动使用tty_alloc_device()函数。终端设备驱动围绕tty_driver展开,通常包含tty驱动的模块加载和卸载函数模板。
Linux终端设备的函数操作包括打开和关闭、数据发送和接收。打开函数由tty_driver中的open()成员函数执行,通常需要设置open()成员,否则返回-ENODEV。关闭函数由tty_driver中的close()成员函数执行。数据发送通过“write()系统调用―tty核心―线路规程”的调用链完成。发送函数应返回实际发送的字节数,若调用期间发生错误,返回负错误码。write()函数接受tty_struct、发送数据指针及字节数参数。当tty系统自己需要发送数据时,如果没有实现put_char()函数,write()函数将被调用,count参数为1。读取操作由tty核心的缓冲逻辑管理,tty_driver无需实现read()函数。当需要将flip缓冲区刷新到用户时,调用tty_flip_buffer_push()函数。从tty驱动接收到字符通过tty_insert_flip_char()函数插入到flip缓冲区,函数接受tty_struct、字符及标志参数,用于设置字符类型。
linux设备驱动程序——设备树(0)-dtb格式
设备树的一般操作方式是:开发人员根据开发需求编写dts文件,然后使用dtc将dts编译成dtb文件。
dts文件是文本格式的文件,而dtb是二进制文件,在linux启动时被加载到内存中,接下来我们需要来分析设备树dtb文件的格式。
为什么要了解设备树dtb文件的格式
dtb作为二进制文件被加载到内存中,然后由内核读取并进行解析,如果对dtb文件的格式不了解,那么在看设备树解析相关的内核代码时将会寸步难行,而阅读源代码才是了解设备树最好的方式,所以,如果需要更透彻的了解设备树解析的细节,第一步就是需要了解设备树的格式。
dtb格式总览
dtb的格式是这样的:
dtb header
但凡涉及到数据的记录,就一定会有一个总的描述部分,就像磁盘的超级块,书的目录,dtb当然也不例外,这个描述头部就是dtb的header部分,通过这个header部分,用户可以快速地了解到整个dtb的大致信息。
header可以用这么一个结构体来描述:
magic
设备树的魔数,魔数其实就是一个用于识别的数字,表示设备树的开始,linux dtb的魔数为 0xddfeed.
totalsize
这个设备树的size,也可以理解为所占用的实际内存空间。
off_dt_struct
offset to dt_struct,表示整个dtb中structure部分所在内存相对头部的偏移地址
off_dt_strings
offset to dt_string,表示整个dtb中string部分所在内存相对头部的偏移地址
off_mem_rsvmap
offset to memory reserve map,dtb中memory reserve map所在内存相对头部的偏移地址,
version
设备树的版本,截至目前的最新版本为.
last_comp_version
最新的兼容版本
boot_cpuid_phys
这部分仅在版本2中存在,后续版本不再使用。
size_dt_strings
表示整个dtb中string部分的大小
size_dt_struct
表示整个dtb中struct部分的大小
alignment gap
中间的alignment gap部分表示对齐间隙,它并非是必须的,它是否被提供以及大小由具体的平台对数据对齐和的要求以及数据是否已经对齐来决定。
memory reserve map
memory reserve map:描述保留的内存部分,这个map的数据结构是这样的:
这部分存储了此结构的列表,整个部分的结尾由一个数据为0的结构来表示(即physical_address和size都为0,总共字节)。
这一部分的数据并非是节点中的memory子节点,而是在设备开始之前(也就是第一个花括号之前)定义的,例如:
这一部分的作用是告诉内核哪一些内存空间需要被保留而不应该被系统覆盖使用,因为在内核启动时常常需要动态申请大量的内存空间,只有提前进行注册,用户需要使用的内存才不会被系统征用而造成数据覆盖。
值得一提的是,对于设备树而言,即使不指定保留内存,系统也会默认为设备树保留相应的内存空间。
同时,这一部分需要位(8字节)对齐。
device-tree structure
device-tree structure:每个节点都会被描述为一个struct,节点之间可以嵌套,因此也会有嵌套的struct。
structure的的结构是这样的:
device-tree strings
device-tree strings:在dtb中有大量的重复字符串,比如"model","compatile"等等,为了节省空间,将这些字符串统一放在某个地址,需要使用的时候直接使用索引来查看。
需要注意的是,属性部分格式为key = value,key部分被放置在strings部分,而value部分的字符串并不会放在这一部分,而是直接放在structure中。
dtb文件解析示例
光说不练假把式,下面我就使用一个简单的示例来剖析dtb的文件格式。
下述示例仅仅是一个演示demo,不针对任何平台,为了演示方便,编写了一个非常简单的dts文件。 /dts-v1/; / {
编译当前dts文件,获取对应的dtb文件。
鉴于dtb文件为二进制文件,普通编辑器打开显示乱码,我们使用ultraEdit查看,它将数据以进制形式显示:
整个dtb文件还是比较简单的,图中的红色框出的部分为header部分的数据,可以看到:
整个头部为字节,进制为0x,从头部信息中off_mem_rsvmap部分可以得到,reserve memory起始地址为0x,上文中提到,这一部分使用一个字节的struct来描述,以一个全为0的struct结尾。
后字节全为0,可以看出,这里并没有设置reserve memory。
structure 部分
上文回顾:每一个属性都是以 key = value的形式来描述,value部分可选。
偏移地址来到0x(0x+0x),接下来8个字节为,根据上述structure中的描述,这是OF_DT_PROP,即标示属性的开始。
接下来4字节为,表明该属性的value部分size为字节。
接下来4字节是当前属性的key在string 部分的偏移地址,这里是,由头部信息中off_dt_strings可以得到,string部分的开始为,偏移地址为0,所以对应字符串为"compatible".
之后就是value部分,这部分的数据是字符串,可以直接从右侧栏看出,总共字节的字符串"hd,test_dts", "hd,test_xxx",因为字符串之间以0结尾,所以程序可以识别出这是两个字符串。
可以看出,到这里,compatible = "hd,test_dts", "hd,test_xxx";这个属性就被描述完了,对于属性的描述还是非常简单的。
按照固有的规律,接下来就是对#address-cells = <0x1>的解析,然后是#size-cells = <0x1>...
然后就是递归的子节点chosen,memory@等等都是按照上文中提到的structure解析规则来进行解析,最后以结尾。
与根节点不同的是,子节点有一个unit name,即chosen,memory@这些名称,并非节点中的.name属性。
而整个结构的结束由来描述。
一般而言,在位系统中,dtc在编译dts文件时会自动考虑对齐问题,所以对于设备树的对齐字节,我们只需要有所了解即可,并不会常接触到。
好了,关于linux设备树dtb文件格式的讨论就到此为止啦。