手把手要教你编写Linux设备驱动程序
- 格式:doc
- 大小:72.50 KB
- 文档页数:13
linux驱动开发(⼀)1:驱动开发环境要进⾏linux驱动开发我们⾸先要有linux内核的源码树,并且这个linux内核的源码树要和开发板中的内核源码树要⼀直;⽐如说我们开发板中⽤的是linux kernel内核版本为2.6.35.7,在我们ubuntu虚拟机上必须要有同样版本的源码树,我们再编译好驱动的的时候,使⽤modinfo XXX命令会打印出⼀个版本号,这个版本号是与使⽤的源码树版本有关,如果开发板中源码树中版本与modinfo的版本信息不⼀致使⽆法安装驱动的;我们开发板必须设置好nfs挂载;这些在根⽂件系统⼀章有详细的介绍;2:开发驱动常⽤的⼏个命令lsmod :list moduel 把我们机器上所有的驱动打印出来,insmod:安装驱动rmmod:删除驱动modinfo:打印驱动信息3:写linux驱动⽂件和裸机程序有很⼤的不同,虽然都是操作硬件设备,但是由于写裸机程序的时候是我们直接写代码操作硬件设备,这只有⼀个层次;⽽我们写驱动程序⾸先要让linux内核通过⼀定的接⼝对接,并且要在linux内核注册,应⽤程序还要通过内核跟应⽤程序的接⼝相关api来对接;4:驱动的编译模式是固定的,以后编译驱动的就是就按照这个模式来套即可,下⾯我们来分下⼀下驱动的编译规则:#ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个#KERN_VER = $(shell uname -r)#KERN_DIR = /lib/modules/$(KERN_VER)/build# 开发板的linux内核的源码树⽬录KERN_DIR = /root/driver/kernelobj-m += module_test.oall:make -C $(KERN_DIR) M=`pwd` modulescp:cp *.ko /root/porting_x210/rootfs/rootfs/driver_test.PHONY: cleanclean:make -C $(KERN_DIR) M=`pwd` modules cleanmake -C $(KERN_DIR) M=`PWD` modules这句话代码的作⽤就是到 KERN_DIR这个⽂件夹中 make modules把当前⽬录赋值给M,M作为参数传到主⽬录的Makefile中,实际上是主⽬录的makefile中有⽬标modules,下⾯有⼀定的规则来编译驱动;#KERN_VER = $(shell uname -r)#KERN_DIR = /lib/modules/$(KERN_VER)/build我们在ubuntu中编译内核的时候⽤这两句代码,因为在ubuntu中为我们保留了⼀份linux内核的源码树,我们编译的时候直接调⽤那个源码树的主Makefile以及⼀些头⽂件、内核函数等;了解规则以后,我们设置好KERN_DIR、obj-m这两个变量以后直接make就可以了;经过编译会得到下⾯⼀些⽂件:下⾯我们可以使⽤lsmod命令来看⼀下我们ubuntu机器现有的⼀些驱动可以看到有很多的驱动,下⾯我们使⽤insmod XXX命令来安装驱动,在使⽤lsmod命令看⼀下实验现象可以看到我们刚才安装的驱动放在了第⼀个位置;使⽤modinfo来打印⼀下驱动信息modinfo xxx.ko这⾥注意vermagic 这个的1.8.0-41是你⽤的linux内核源码树的版本号,只有这个编译的版本号与运⾏的linux内核版本⼀致的时候,驱动程序才会被安装注意license:GPL linux内核开元项⽬的许可证⼀般都是GPL这⾥尽量设置为GPL,否则有些情况下会出现错误;下⾯使⽤rmmod xxx删除驱动;-------------------------------------------------------------------------------------5:下⾯我们分析⼀下驱动。
Linux设备驱动程序原理及框架-内核模块入门篇内核模块介绍应用层加载模块操作过程内核如何支持可安装模块内核提供的接口及作用模块实例内核模块内核模块介绍Linux采用的是整体式的内核结构,这种结构采用的是整体式的内核结构,采用的是整体式的内核结构的内核一般不能动态的增加新的功能。
为此,的内核一般不能动态的增加新的功能。
为此,Linux提供了一种全新的机制,叫(可安装) 提供了一种全新的机制,可安装) 提供了一种全新的机制模块” )。
利用这个机制“模块”(module)。
利用这个机制,可以)。
利用这个机制,根据需要,根据需要,在不必对内核重新编译链接的条件将可安装模块动态的插入运行中的内核,下,将可安装模块动态的插入运行中的内核,成为内核的一个有机组成部分;成为内核的一个有机组成部分;或者从内核移走已经安装的模块。
正是这种机制,走已经安装的模块。
正是这种机制,使得内核的内存映像保持最小,的内存映像保持最小,但却具有很大的灵活性和可扩充性。
和可扩充性。
内核模块内核模块介绍可安装模块是可以在系统运行时动态地安装和卸载的内核软件。
严格来说,卸载的内核软件。
严格来说,这种软件的作用并不限于设备驱动,并不限于设备驱动,例如有些文件系统就是以可安装模块的形式实现的。
但是,另一方面,可安装模块的形式实现的。
但是,另一方面,它主要用来实现设备驱动程序或者与设备驱动密切相关的部分(如文件系统等)。
密切相关的部分(如文件系统等)。
课程内容内核模块介绍应用层加载模块操作过程内核如何支持可安装模块内核提供的接口及作用模块实例内核模块应用层加载模块操作过程内核引导的过程中,会识别出所有已经安装的硬件设备,内核引导的过程中,会识别出所有已经安装的硬件设备,并且创建好该系统中的硬件设备的列表树:文件系统。
且创建好该系统中的硬件设备的列表树:/sys 文件系统。
(udev 服务就是通过读取该文件系统内容来创建必要的设备文件的。
)。
如何在Linux系统中安装驱动程序Linux系统作为一个开源的操作系统,广泛应用于各种设备和领域。
而安装驱动程序是在Linux系统中使用外部硬件设备的关键步骤之一。
在本文中,我们将学习如何在Linux系统中安装驱动程序的方法和步骤。
1. 检查硬件设备在安装驱动程序之前,首先需要确定硬件设备的型号和制造商。
可以通过查询设备的型号或者查看设备的相关文档来获取这些信息。
这是非常重要的,因为不同的设备可能需要不同的驱动程序来正确地工作。
2. 更新系统在安装驱动程序之前,确保你的Linux系统已经是最新的状态。
可以通过在终端中运行以下命令来更新系统:```sudo apt-get updatesudo apt-get upgrade```更新系统可以确保你拥有最新的软件包和驱动程序,以获得更好的兼容性和性能。
3. 查找合适的驱动程序一般来说,大部分硬件设备的驱动程序都可以在Linux系统的软件仓库中找到。
可以通过使用包管理器(如apt、yum等)来查找并安装合适的驱动程序。
运行以下命令来搜索并安装特定的驱动程序:```sudo apt-cache search 驱动程序名称sudo apt-get install 驱动程序名称```注意替换“驱动程序名称”为具体的驱动程序名称。
安装驱动程序可能需要输入管理员密码和确认安装。
如果你无法在软件仓库中找到合适的驱动程序,可以转向设备的制造商网站或者开源社区来获取。
下载驱动程序后,根据驱动程序提供的文档和说明来安装。
4. 编译和安装驱动程序有些驱动程序可能需要手动编译和安装。
在这种情况下,你需要确保你的系统已经安装了编译工具(如GCC、make等)。
在终端中切换到驱动程序所在的目录,并按照以下步骤进行编译和安装:```./configuremakesudo make install```以上命令将分别进行配置、编译和安装驱动程序。
在进行安装之前,可能需要输入一些配置选项或者确认安装。
⼀、如何编写LinuxPCI驱动程序PCI的世界是⼴阔的,充满了(⼤部分令⼈不快的)惊喜。
由于每个CPU体系结构实现不同的芯⽚集,并且PCI设备有不同的需求(“特性”),因此Linux内核中的PCI⽀持并不像⼈们希望的那么简单。
这篇简短的⽂章介绍⽤于PCI设备驱动程序的Linux APIs。
1.1 PCI驱动程序结构PCI驱动程序通过pci_register_driver()在系统中"发现"PCI设备。
事实上,恰恰相反。
当PCI通⽤代码发现⼀个新设备时,具有匹配“描述”的驱动程序将被通知。
详情如下。
pci_register_driver()将设备的⼤部分探测留给PCI层,并⽀持在线插⼊/删除设备[因此在单个驱动程序中⽀持热插拔PCI、CardBus和Express-Card]。
pci_register_driver()调⽤需要传⼊⼀个函数指针表,从⽽指⽰驱动程序的更⾼⼀级结构体。
⼀旦驱动程序知道了⼀个PCI设备并获得了所有权,驱动程序通常需要执⾏以下初始化:启⽤设备请求MMIO / IOP资源设置DMA掩码⼤⼩(⽤于⼀致性DMA和流式DMA)分配和初始化共享控制数据(pci_allocate_coherent())访问设备配置空间(如果需要)注册IRQ处理程序(request_irq())初始化non-PCI(即LAN/SCSI/等芯⽚部分)启⽤DMA /处理引擎当使⽤设备完成时,可能需要卸载模块,驱动程序需要采取以下步骤:禁⽌设备产⽣irq释放IRQ (free_irq())停⽌所有DMA活动释放DMA缓冲区(包括流式DMA和⼀致性DMA)从其他⼦系统注销(例如scsi或netdev)释放MMIO / IOP资源禁⽤该设备下⾯⼏节将介绍这些主题中的⼤部分。
其余部分请查看LDD3或<linux/pci.h>。
如果PCI⼦系统没有配置(没有设置CONFIG_PCI),下⾯描述的⼤多数PCI函数都被定义为内联函数,要么完全空,要么只是返回⼀个适当的错误代码,以避免在驱动程序中出现⼤量ifdefs。
Linux视频设备驱动编程(v4l2编程)一.什么是video4linuxVideo4linux2(简称V4L2),是linux中关于视频设备的内核驱动。
在Linux 中,视频设备是设备文件,可以像访问普通文件一样对其进行读写,摄像头在/dev/video0下。
二、一般操作流程(视频设备):1. 打开设备文件。
int fd=open(”/dev/video0″,O_RDWR);2. 取得设备的capability,看看设备具有什么功能,比如是否具有视频输入,或者音频输入输出等。
VIDIOC_QUERYCAP,struct v4l2_capability3. 选择视频输入,一个视频设备可以有多个视频输入。
VIDIOC_S_INPUT,struct v4l2_input4. 设置视频的制式和帧格式,制式包括PAL,NTSC,帧的格式个包括宽度和高度等。
VIDIOC_S_STD,VIDIOC_S_FMT,struct v4l2_std_id,struct v4l2_format5. 向驱动申请帧缓冲,一般不超过5个。
struct v4l2_requestbuffers6. 将申请到的帧缓冲映射到用户空间,这样就可以直接操作采集到的帧了,而不必去复制。
mmap7. 将申请到的帧缓冲全部入队列,以便存放采集到的数据.VIDIOC_QBUF,struct v4l2_buffer8. 开始视频的采集。
VIDIOC_STREAMON9. 出队列以取得已采集数据的帧缓冲,取得原始采集数据。
VIDIOC_DQBUF10. 将缓冲重新入队列尾,这样可以循环采集。
VIDIOC_QBUF11. 停止视频的采集。
VIDIOC_STREAMOFF12. 关闭视频设备。
close(fd);三、常用的结构体(参见/usr/include/linux/videodev2.h):struct v4l2_requestbuffers reqbufs;//向驱动申请帧缓冲的请求,里面包含申请的个数struct v4l2_capability cap;//这个设备的功能,比如是否是视频输入设备struct v4l2_input input; //视频输入struct v4l2_standard std;//视频的制式,比如PAL,NTSCstruct v4l2_format fmt;//帧的格式,比如宽度,高度等struct v4l2_buffer buf;//代表驱动中的一帧v4l2_std_id stdid;//视频制式,例如:V4L2_STD_PAL_Bstruct v4l2_queryctrl query;//查询的控制struct v4l2_control control;//具体控制的值下面具体说明开发流程(网上找的啦,也在学习么)打开视频设备在V4L2中,视频设备被看做一个文件。
想要成为Linux底层驱动开发高手这些技巧绝对不能错过对于想要成为Linux底层驱动开发高手的人来说,掌握一些关键技巧是非常重要的。
本文将介绍一些不能错过的技巧,帮助读者提升自己在Linux底层驱动开发领域的能力。
1. 深入理解Linux内核:在成为Linux底层驱动开发高手之前,你需要对Linux内核有深入的理解。
了解内核的基本概念、代码结构和内核模块之间的关系是非常重要的。
阅读Linux内核的源代码、参与内核邮件列表的讨论以及阅读相关的文献资料都是提升自己技能的好途径。
2. 熟悉底层硬件知识:作为底层驱动开发者,你需要熟悉底层硬件的工作原理。
这包括了解处理器架构、设备的寄存器操作、中断处理等。
掌握底层硬件知识可以帮助你编写高效、稳定的驱动程序。
3. 学习使用适当的开发工具:在Linux底层驱动开发中,使用适当的开发工具是非常重要的。
例如,使用调试器可以帮助你快速定位驱动程序中的问题。
掌握使用GCC编译器、GNU调试器(GDB)和性能分析工具(如OProfile)等工具可以提高你的开发效率。
4. 阅读相关文档和源代码:Linux底层驱动开发涉及到大量的文档和源代码。
阅读设备供应商提供的文档、Linux内核源代码以及其他相关文献资料可以帮助你更好地了解特定设备的工作原理和使用方法。
5. 编写清晰、高效的代码:编写清晰、高效的代码对于成为Linux底层驱动开发高手是至关重要的。
使用良好的编码风格、注释和命名规范可以提高代码的可读性。
此外,了解Linux内核的设计原则和最佳实践也是编写高质量驱动程序的关键。
6. 多实践、调试和优化:在实际开发过程中,积累经验是非常重要的。
通过多实践、调试和优化不同类型的驱动程序,你可以更好地理解Linux底层驱动开发的技巧和要点。
此外,学会使用内核调试工具和性能分析工具可以帮助你提高驱动程序的质量和性能。
7. 参与开源社区:参与开源社区是成为Linux底层驱动开发高手的好方法。
linux 开发新驱动步骤Linux作为一款开源的操作系统,其内核源码也是开放的,因此,许多开发人员在Linux上进行驱动开发。
本文将介绍在Linux上进行新驱动开发的步骤。
第一步:确定驱动类型和接口在进行驱动开发前,需要确定驱动类型和接口。
驱动类型包括字符设备驱动、块设备驱动、网络设备驱动等。
接口包括设备文件、系统调用、ioctl等。
根据驱动类型和接口的不同,驱动开发的流程也有所不同。
第二步:了解Linux内核结构和API驱动开发需要熟悉Linux内核的结构和API。
Linux内核由许多模块组成,每个模块都有自己的功能。
API是应用程序接口,提供了许多函数和数据结构,开发人员可以使用这些函数和数据结构完成驱动开发。
第三步:编写驱动代码在了解了Linux内核结构和API后,就可以编写驱动代码了。
驱动代码需要按照Linux内核的编码规范编写,确保代码风格统一、可读性好、可维护性强等。
在编写代码时,需要使用API提供的函数和数据结构完成相应的功能。
第四步:编译驱动代码和内核模块驱动代码编写完成后,需要编译成内核模块。
编译内核模块需要使用内核源码中的Makefile文件。
编译完成后,会生成一个.ko文件,这个文件就是内核模块。
第五步:加载和卸载内核模块内核模块编译完成后,需要加载到Linux系统中。
可以使用insmod命令加载内核模块,使用rmmod命令卸载内核模块。
在加载和卸载内核模块时,需要注意依赖关系,确保依赖的模块已经加载或卸载。
第六步:调试和测试驱动开发完成后,需要进行调试和测试。
可以使用printk函数输出调试信息,在/var/log/messages文件中查看。
测试时需要模拟各种可能的情况,确保驱动程序的稳定性和可靠性。
Linux驱动开发需要掌握Linux内核结构和API,熟悉驱动类型和接口,按照编码规范编写驱动代码,并进行编译、加载、调试和测试。
只有掌握了这些技能,才能进行高效、稳定和可靠的驱动开发。
Linux设备驱动开发入门本文以快捷而简单的方式讲解如何像一个内核开发者那样开发linux设备驱动源作者: Xavier Calbet版权:GNU Free Documentation License 翻译: 顾宏军()中文版权:创作共用.署名-非商业用途-保持一致知识准备要开发Linux 设备驱动,需要掌握以下知识:•C 编程 需要掌握深入一些的C 语言知识,比如,指针的使用,位处理函数,等。
•微处理器编程 需要理解微机的内部工作原理:存贮器地址,中断,等。
这些内容对一个汇编程序员应该比较熟悉。
Linux 下有好几种不同的设备。
为简单起见,本文只涉及以模块形式加载的字符设备。
使用2.6.x 的内核。
(特别是Debian Sarge 使用的2.6.8内核。
)用户空间和内核空间当你开发设备驱动时,需要理解“用户空间”和内核空间之间的区别。
1:2:3:4:5:6:7:8:9:10:11:12:13:14:15:16:17:18:19:20:21:22:23:24:25:•内核空间 :Linux 操作系统,特别是它的内核,用一种简单而有效的方法管理机器的硬件,给用户提供一个简捷而统一的编程接口。
同样的,内核,特别是它的设备驱动程序,是连接最终用户/程序员和硬件的一坐桥或者说是接口。
任何子程序或者函数只要是内核的一部分(例如:模块,和设备驱动),那它也就是内核空间的一部分。
•用户空间. 最终用户的应用程序,像UNIX 的shell 或者其它的GUI 的程序(例如,gedit),都是用户空间的一部分。
很显然,这些应用程序需要和系统的硬件进行交互。
但是,他们不是直接进行,而是通过内核支持的函数进行。
它们的关系可以通过下图表示:图1: 应用程序驻留在用户空间, 模块和设备驱动驻留在内核空间26:27:28:29:30:31:32:33:34:35:36:37:38:39:40:用户空间和内核空间之间的接口函数内核在用户空间提供了很多子程序或者函数,它们允许用户应用程序员和硬件进行交互。
驱动编程1 模块的概述 (2)2 source insight 加载内核源码方法 (2)3 模块makefile的编写 (3)4 模块makefile编写方法 (4)5 在X86上运行模块: (5)6 编写模块 (5)7 模块的加载进内核命令 (5)8 最简单的上层调用+ 调用驱动方法 (6)9 复杂框架上层应用+驱动调用方法 (7)10 复杂框架字符设备创建并注册过程 (7)11 file_operations常用函数 (9)12 同步互斥操作 (10)13 同步互斥函数总结 (10)14 阻塞IO编程流程 (11)15 轮询操作上层select 下层poll (12)16 信号处理 (12)17 中断 (13)18 中断新模型--上半部中断和下半部中断的实现 (14)19 内核定时器编程 (15)20 内核延时函数 (15)21 内核源代码中头文件分配方式 (15)22 linux内核管理和内核的内存管理 (16)23 设备io端口和io内存访问–如何控制led的亮灭 (16)24 * 驱动-设备分离思想编程————内核进阶 (18)25 驱动-设备分离-核心最小架构 (18)26 驱动设备分离思想- 上层架构(基于封装) (20)27 头文件总结 (24)28 设置系统自启动命令u-boot (24)第一天需要理清的东西1)模块的概念,模块与应用的区别2)模块主要的组成头文件、module_init() modoule_exit() module_lisence()3)模块的如何编辑,如何编译,如何加载到内核中运行使用makefile4)模块驱动编写,必须通过上层应用程序调用。
1模块的概述模块是内核的一部分,为了防止内核太大,把它放在文件系统里面。
也可以在编译内核的直接编译进内核。
1,存储位置可以在开始时编译进内核,也可以编译进模块,最后加载2、运行时环境在哪个内核树下编译,就对应这个运行环境3、模块的编译问题:前提条件是需要对应的内核源码树,或者必须有对应的内核版本匹配4、模块编译使用makefile 注意makefile的编写2source insight 加载内核源码方法在windows下创建工程,使用source insight查看内核代码:2.1 先将内核源码拷到对应的文件夹2.2 在source insight 里添加工程,筛选需要添加的文件注意选择按照树来添加,然后按照remove来踢出不需要的文件夹2.3 最后同步3模块makefile的编写模块的编译:1)、模块编译的核心语句:$(MAKE) -C $(KERNELDIR) M=$(PWD) modules-C :进入内核源码树M= : 返回到当前目录,再次执行该目录下的makefileeg: /tnt/abc.c -----> abc.ko1、在/tnt目录下敲make,只有/tnt目录下的Makefile被调用2、目的是要进入到内核源码树中,一进一回,-C来进,M=带着内核源码树中的makefile的功能回来了-------内核源码树中Makefile的目标:obj-y:汇集了所有编译进内核的目标文件obj-m:汇集了所有编译成模块的目标文件3、回来过后,我们只有确定obj-m变量的集合4、make modules告诉内核的makefile,只做编译模块的功能4模块makefile编写方法ifeq ($(KERNELRELEASE),)KERNELDIR := /work/linux-2.6.35-farsightPWD := $(shell pwd)modules:$(MAKE) -C $(KERNELDIR) M=$(PWD) modulesinstall:$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_installclean:rm -rf .tmp_versions *.ko *.o .*.cmd *.mod.c *.order *.symvers.PHONY: modules cleanelseobj-m := ex1.oendif以上是makefile的内容,●注意原来的内核目录树不要进行make clean 或者make distclean●KERNELDIR 表示模块加载在哪个内核的文件夹(又叫内核源码树),●$(MAKE) -C $(KERNELDIR) M=$(PWD) modules 表示进入该内核文件夹,将顶层makefile 中的内容带回,再重新执行一次该makefile 将obj-m := ex1.o 编译,并执行make modules (并只编译ex1.c ,不编译其它模块)●$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install 表示执行顶层makefile的modules install 标签下的命令●安装的位置原来默认在/lib 下面,所以需要修改其到我们制作的根文件系统下/work/rootfs/在顶层Makefile位置搜索:MODLIB修改为:●obj-m := ex1.o 你需要编译的.c的文件名****************************此时简单的编译环境已经搭建完毕******************* ****************************执行make ***********************************************执行make install *******************在/work/rootfs/lib/modules/2.6.35/extra即可找到该模块.ko*****************************************************************************搭建好环境,保证虚拟机与板子与计算机网络连通,并设置板子u-boot 从nfs挂载,启动内核,并成功通过nfs 加载rootfs,此时环境完毕,进入/work/rootfs/lib/modules/2.6.35/extra ,找到模块,加载卸装模块操纵5在X86上运行模块:修改Makefile中的内核源码树的目录X86下的内核源码树:/lib/modules/2.6.35-22-generic/build如果没有在控制台上交互,默认是看不到信息的,需要dmesg这个命令去查看6编写模块模块最小组成如下:●注意:module_init module_exit 必须放在末尾●注意:函数的原型返回值●头文件7模块的加载进内核命令insmodrmmodlsmod8最简单的上层调用+ 调用驱动方法8.1 首先在module_init(abc) abc函数中注册设备register_chrdev(注册设备号,上层可见的设备名,操作封装)该函数完成设备注册,在板子上用cat /proc/devices 便可以看见该设备8.2 完成fops 操作的封装●注意格式●必须在函数后面声明该结构体●头文件#include <linux/fs.h>8.3 查看到该字符设备后,创建设备节点,则上层通过设备字符名与该设备号绑定mknod /dev/hf_char c 245 0ls /dev/ 可以查看注册的所有设备节点8.4 此时上层应用的open(”hf_char”,O_RDWR),即可完成该设备的打开,即可以完成上层应用于下层驱动相关fops 的操作。
Linux驱动之LCD驱动编写在这篇博客中已经分析了编写LCD驱动的步骤,接下来就按照这个步骤来字尝试字节编写LCD驱动。
⽤的LCD屏幕为tft 屏,每个像素点为16bit。
对应与红绿蓝分别为565。
1、分配⼀个fb_info结构2、设置fb_info结构3、硬件相关的操作,配置LCD时钟、配置IO端⼝、配置LCD寄存器。
4、最终注册fbinfo结构到registered_fb数组要理解LCD的⼯作原理,需要了解LCD的时钟,在TFT的LCD中有如下的时钟。
这个⼏个时钟数据在配置LCD寄存器时都说需要设置的。
1、VCLK:两个像素之间的时钟,即两个像素隔多长时间才能显⽰下⼀个像素2、HSYNC:⽔平同步时钟,即第⼀⾏像素点显⽰完成之后隔多长时间才能开始下⼀⾏的显⽰3、VSYNC:垂直⽅向的同步时钟,也叫帧同步信号,即⼀帧数据显⽰完成之后(⼀帧数据表⽰⼀个屏幕显⽰完成,即⼀个显存的数据全部取完),过多长下⼀帧数据才开始显⽰本节需要⽤到的函数:void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp); //分配DMA缓存区给显存//返回值为:申请到的DMA缓冲区的虚拟地址,若为NULL,表⽰分配失败,则需要使⽤dma_free_writecombine()释放内存,避免内存泄漏//参数如下://*dev:指针,这⾥填0,表⽰这个申请的缓冲区⾥没有内容//size:分配的地址⼤⼩(字节单位)//*handle:申请到的物理起始地址//gfp:分配出来的内存参数,标志定义在<linux/gfp.h>,常⽤标志如下://GFP_ATOMIC ⽤来从中断处理和进程上下⽂之外的其他代码中分配内存. 从不睡眠.//GFP_KERNEL 内核内存的正常分配. 可能睡眠.//GFP_USER ⽤来为⽤户空间页来分配内存; 它可能睡眠.分配⼀段DMA缓存区,分配出来的内存会禁⽌cache缓存(因为DMA传输不需要CPU)它和 dma_alloc_coherent ()函数相似,不过 dma_alloc_coherent ()函数是分配出来的内存会禁⽌cache缓存以及禁⽌写⼊缓冲区dma_free_writecombine(dev,size,cpu_addr,handle); //释放缓存//cpu_addr:虚拟地址,//handle:物理地址释放DMA缓冲区, dev和size参数和上⾯的⼀样struct fb_info *framebuffer_alloc(size_t size, struct device *dev); //申请⼀个fb_info结构体,//size:额外的内存,//*dev:指针, 这⾥填0,表⽰这个申请的结构体⾥没有内容int register_framebuffer(struct fb_info *fb_info);//向内核中注册fb_info结构体,若内存不够,注册失败会返回负数int unregister_framebuffer(struct fb_info *fb_info) ;//注销内核中fb_info结构体本节需要⽤到的结构体:fb_info结构体如下:struct fb_info {... ...struct fb_var_screeninfo var; //可变的参数struct fb_fix_screeninfo fix; //固定的参数... ...struct fb_ops *fbops; //操作函数... ...char __iomem *screen_base; //显存虚拟起始地址unsigned long screen_size; //显存虚拟地址长度void *pseudo_palette;//假的16⾊调⾊板,⾥⾯存放了16⾊的数据,可以通过8bpp数据来找到调⾊板⾥⾯的16⾊颜⾊索引值,模拟出16⾊颜⾊来,节省内存,不需要的话就指向⼀个不⽤的数组即可 ... ...};其中操作函数fb_info-> fbops 结构体写法如下:static struct fb_ops s3c_lcdfb_ops = {.owner = THIS_MODULE,.fb_setcolreg = my_lcdfb_setcolreg,//设置调⾊板fb_info-> pseudo_palette,⾃⼰构造该函数.fb_fillrect = cfb_fillrect, //填充矩形,⽤/drivers/video/ cfbfillrect.c⾥的函数即可.fb_copyarea = cfb_copyarea, //复制数据, ⽤/drivers/video/cfbcopyarea.c⾥的函数即可.fb_imageblit = cfb_imageblit, //绘画图形, ⽤/drivers/video/imageblit.c⾥的函数即可};固定的参数fb_info-> fix 结构体如下:struct fb_fix_screeninfo {char id[16]; //id名字unsigned long smem_start; //framebuffer物理起始地址__u32 smem_len; //framebuffer长度,字节为单位__u32 type; //lcd类型,默认值0即可__u32 type_aux; //附加类型,为0__u32 visual; //画⾯设置,常⽤参数如下// FB_VISUAL_MONO01 0 单⾊,0:⽩⾊,1:⿊⾊// FB_VISUAL_MONO10 1 单⾊,1:⽩⾊,0:⿊⾊// FB_VISUAL_TRUECOLOR 2 真彩(TFT:真彩)// FB_VISUAL_PSEUDOCOLOR 3 伪彩// FB_VISUAL_DIRECTCOLOR 4 直彩 __u16 xpanstep; /*如果没有硬件panning就赋值为0 */ __u16 ypanstep; /*如果没有硬件panning就赋值为0 */ __u16 ywrapstep; /*如果没有硬件ywrap就赋值为0 */ __u32 line_length; /*⼀⾏的字节数 ,例:(RGB565)240*320,那么这⾥就等于240*16/8 */ /*以下成员都可以不需要*/ unsigned long mmio_start; /*内存映射IO的起始地址,⽤于应⽤层直接访问寄存器,可以不需要*/__u32 mmio_len; /* 内存映射IO的长度,可以不需要*/__u32 accel;__u16 reserved[3];};可变的参数fb_info-> var 结构体如下:structfb_var_screeninfo{ __u32xres; /*可见屏幕⼀⾏有多少个像素点*/__u32 yres; /*可见屏幕⼀列有多少个像素点*/__u32 xres_virtual; /*虚拟屏幕⼀⾏有多少个像素点 */__u32 yres_virtual; /*虚拟屏幕⼀列有多少个像素点*/__u32 xoffset; /*虚拟到可见屏幕之间的⾏偏移,若可见和虚拟的分辨率⼀样,就直接设为0*/ __u32 yoffset; /*虚拟到可见屏幕之间的列偏移*/__u32 bits_per_pixel; /*每个像素的位数即BPP,⽐如:RGB565则填⼊16*/__u32 grayscale; /*⾮0时,指的是灰度,真彩直接填0即可*/struct fb_bitfield red; //fb缓存的R位域, fb_bitfield结构体成员如下://__u32 offset; 区域偏移值,⽐如RGB565中的R,就在第11位//__u32 length; 区域长度,⽐如RGB565的R,共有5位//__u32 msb_right; msb_right ==0,表⽰数据左边最⼤, msb_right!=0,表⽰数据右边最⼤struct fb_bitfield green; /*fb缓存的G位域*/struct fb_bitfield blue; /*fb缓存的B位域*/ /*以下参数都可以不填,默认为0*/struct fb_bitfield transp; /*透明度,不需要填0即可*/__u32nonstd; /* != 0表⽰⾮标准像素格式*/__u32 activate; /*设为0即可*/__u32height; /*外设⾼度(单位mm),⼀般不需要填*/__u32width; /*外设宽度(单位mm),⼀般不需要填*/__u32 accel_flags; /*过时的参数,不需要填*//* 除了pixclock本⾝外,其他的都以像素时钟为单位*/__u32pixclock; /*像素时钟(⽪秒)*/__u32 left_margin; /*⾏切换,从同步到绘图之间的延迟*/__u32right_margin; /*⾏切换,从绘图到同步之间的延迟*/__u32upper_margin; /*帧切换,从同步到绘图之间的延迟*/__u32lower_margin; /*帧切换,从绘图到同步之间的延迟*/__u32hsync_len; /*⽔平同步的长度*/__u32 vsync_len; /*垂直同步的长度*/__u32 sync;__u32 vmode;__u32 rotate;__u32reserved[5]; /*保留*/}1.写驱动程序:(驱动设置:参考⾃带的LCD平台驱动drivers/video/s3c2410fb.c )1.1 步骤如下:在驱动init⼊⼝函数中:1)分配⼀个fb_info结构体2)设置fb_info 2.1)设置固定的参数fb_info-> fix 2.2) 设置可变的参数fb_info-> var 2.3) 设置操作函数fb_info-> fbops 2.4) 设置fb_info 其它的成员3)设置硬件相关的操作 3.1)配置LCD引脚 3.2)根据LCD⼿册设置LCD控制器 3.3)分配显存(framebuffer),把地址告诉LCD控制器和fb_info4)开启LCD,并注册fb_info: register_framebuffer() 4.1) 直接在init函数中开启LCD(后⾯讲到电源管理,再来优化) 控制LCDCON5允许PWREN信号, 然后控制LCDCON1输出PWREN信号, 输出GPB0⾼电平来开背光, 4.2) 注册fb_info在驱动exit出⼝函数中:1)卸载内核中的fb_info2) 控制LCDCON1关闭PWREN信号,关背光,iounmap注销地址3)释放DMA缓存地址dma_free_writecombine()4)释放注册的fb_info1.2 具体代码如下:#include <linux/module.h>#include <linux/kernel.h>#include <linux/fs.h>#include <linux/init.h>#include <asm/io.h> //含有iomap函数iounmap函数#include <asm/uaccess.h>//含有copy_from_user函数#include <linux/device.h>//含有类相关的处理函数#include <linux/fb.h> //含有fb_info结构体定义//#include <asm/dma-mapping.h> //含有dma_free_writecombine宏定义#include <linux/dma-mapping.h> //含有dma_free_writecombine宏定义#include <linux/platform_device.h>//含有平台设备总线模型相关变量#include <linux/mm.h>#include <linux/slab.h>//#include <linux/module.h>//#include <linux/kernel.h>//#include <linux/errno.h>//#include <linux/string.h>//#include <linux/mm.h>//#include <linux/slab.h>//#include <linux/delay.h>//#include <linux/fb.h>//#include <linux/init.h>//#include <linux/dma-mapping.h>//#include <linux/interrupt.h>//#include <linux/workqueue.h>//#include <linux/wait.h>//#include <linux/platform_device.h>//#include <linux/clk.h>//#include <asm/io.h>//#include <asm/uaccess.h>//#include <asm/div64.h>//#include <asm/mach/map.h>//#include <asm/arch/regs-lcd.h>//#include <asm/arch/regs-gpio.h>//#include <asm/arch/fb.h>/*lcd控制寄存器放在⼀个结构体⾥⾯*/struct lcd_regs {unsigned long lcdcon1;unsigned long lcdcon2;unsigned long lcdcon3;unsigned long lcdcon4;unsigned long lcdcon5;unsigned long lcdsaddr1;unsigned long lcdsaddr2;unsigned long lcdsaddr3;unsigned long redlut;unsigned long greenlut;unsigned long bluelut;unsigned long reserved[9];unsigned long dithmode;unsigned long tpal;unsigned long lcdintpnd;unsigned long lcdsrcpnd;unsigned long lcdintmsk;unsigned long lpcsel;};static struct fb_info *s3c_mylcdfb_info;//fb_info结构体static volatile unsigned long *gpbcon;//GPB0⽤于lcd背光的控制static volatile unsigned long *gpbdat;//GPB0⽤于lcd背光的控制static volatile unsigned long *gpccon;static volatile unsigned long *gpdcon;static volatile unsigned long *gpgcon;//GPG4⽤于lcd电源static volatile struct lcd_regs* lcd_regs;//lcd寄存器static u32 pseudo_palette[16]; //调⾊板内存/* from pxafb.c */static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf){chan &= 0xffff; //取出16bit的数据chan >>= 16 - bf->length; //return chan << bf->offset;}static int s3c_mylcdfb_setcolreg(unsigned int regno, unsigned int red,unsigned int green, unsigned int blue,unsigned int transp, struct fb_info *info){unsigned int val;if (regno > 16)return1;/* ⽤red,green,blue三原⾊构造出val */val = chan_to_field(red, &info->var.red);val |= chan_to_field(green, &info->var.green);val |= chan_to_field(blue, &info->var.blue);//((u32 *)(info->pseudo_palette))[regno] = val;pseudo_palette[regno] = val;return0;}static struct fb_ops s3c_mylcdfb_ops = { //操作函数结构体.owner = THIS_MODULE,.fb_setcolreg = s3c_mylcdfb_setcolreg,//待会设置,这个是调⾊板,如果使⽤⼩于16bit的像素需要⽤到 .fb_fillrect = cfb_fillrect,.fb_copyarea = cfb_copyarea,.fb_imageblit = cfb_imageblit,};static int lcd_drv_init(void){/*1、分配⼀个fb_info*/s3c_mylcdfb_info = framebuffer_alloc(0,NULL);//size为额外分配的⼤⼩,这⾥不需要,所以设为0if(s3c_mylcdfb_info==NULL){printk("unframebuffer_alloc\n");return1;}/*2、设置*//*2.1 设置固定的参数*/strcpy(s3c_mylcdfb_info->fix.id, "mylcd");//名字//s3c_mylcdfb_info->fix.smem_start = ;//显存的物理起始地址,后⾯设置s3c_mylcdfb_info->fix.smem_len = 480*272*16/8;//单位为字节,每个像素点占⽤16bit :565,显存的⼤⼩ s3c_mylcdfb_info->fix.type = FB_TYPE_PACKED_PIXELS;//LCD类型,填充像素的类型 tft//s3c_mylcdfb_info->fix.type_aux= //附加的LCD类型,不需要设置s3c_mylcdfb_info->fix.visual = FB_VISUAL_TRUECOLOR;//视觉类型,选择真彩⾊s3c_mylcdfb_info->fix.line_length = 480*16/8; //⼀⾏的长度,单位为字节// s3c_mylcdfb_info->fix.mmio_start = //控制lcd的寄存器的物理地址// s3c_mylcdfb_info->fix.mmio_len = //控制lcd的寄存器的⼤⼩/*2.2 设置可变的参数*/s3c_mylcdfb_info->var.xres = 480;//x⽅向的分辨率s3c_mylcdfb_info->var.yres = 272;//y⽅向的分辨率s3c_mylcdfb_info->var.xres_virtual = 480;//x⽅向的虚拟分辨率s3c_mylcdfb_info->var.yres_virtual = 272;//y⽅向的虚拟分辨率s3c_mylcdfb_info->var.bits_per_pixel = 16;//每个像素的⼤⼩,单位为bits3c_mylcdfb_info->var.grayscale = 0;//灰度值s3c_mylcdfb_info->var.red.length = 5;//红⾊像素占⽤的长度,单位bits3c_mylcdfb_info->var.green.length = 6;//绿⾊像素占⽤的长度,单位bits3c_mylcdfb_info->var.blue.length = 5;//蓝⾊像素占⽤的长度,单位bits3c_mylcdfb_info->var.red.offset= 11;//红⾊像素在16bit中的偏移值s3c_mylcdfb_info->var.green.offset= 6;//绿⾊像素在16bit中的偏移值s3c_mylcdfb_info->var.blue.offset=0;//蓝⾊像素在16bit中的偏移值s3c_mylcdfb_info->var.red.msb_right= 0;//低位在前还是⾼位在前,⼀般⾼位在前,也就是⼩端模式s3c_mylcdfb_info->var.green.msb_right= 0;s3c_mylcdfb_info->var.blue.msb_right=0;s3c_mylcdfb_info->var.activate = FB_ACTIVATE_NOW;//使⽤默认参数,显存⽴刻⽣效/*2.3 设置操作函数*/s3c_mylcdfb_info->fbops = &s3c_mylcdfb_ops;/*2.4 其它的⼀些设置 */s3c_mylcdfb_info->pseudo_palette = pseudo_palette;//调⾊板的地址//s3c_mylcdfb_info->screen_base = ;//显存的虚拟基地址s3c_mylcdfb_info->screen_size = 480*272*16/8;//单位为字节,每个像素点占⽤16bit :565,显存的⼤⼩/*3、硬件相关的操作 *//*3.1、配置GPIO⽤于LCD*/gpbcon = ioremap(0x56000010, 8);//将实际的寄存器地址转换为虚拟地址gpccon = ioremap(0x56000020 , 4);gpdcon = ioremap(0x56000030 , 4);gpgcon = ioremap(0x56000060 , 4);gpbdat = gpbcon + 1;*gpccon = 0xaaaaaaaa; /* GPIO管脚⽤于VD[7:0],LCDVF[2:0],VM,VFRAME,VLINE,VCLK,LEND */*gpdcon = 0xaaaaaaaa; /* GPIO管脚⽤于VD[23:8] */*gpbcon &= ~(3); /* GPB0设置为输出引脚 */*gpbcon |= 1;*gpbdat &= ~1; /* 输出低电平关闭LCD背光 */*gpgcon |= (3<<8); /* GPG4⽤作LCD_PWREN 电源*//*3.2、根据LCD⼿册设置LCD控制器,⽐如VCLK的频率等 */lcd_regs = ioremap(0X4D000000 , sizeof(struct lcd_regs));/** bit[17:8] : VCLK = HCLK / [(CLKVAL+1) x 2]* 10M = 100M/[(CLKVAL+1) x 2]* CLKVAL = 4** bit[6:5] :PNRMODE = 11显⽰模式,选择TFT模式** bit[4:1] :BPPMODE = 1100;像素=16bit 565** bit[0] :ENVID = 0;先关闭LCD控制器*/lcd_regs->lcdcon1 = (4<<8) | (3<<5) | (0x0c<<1);///** [31:24] : VBPD = 帧同步信号发出后,过多长时间开始显⽰数据,单位为⾏,理解为1⾏的时间* 看LCD⼿册tvb = VBPD + 1 = 2;所以VBPD = 1** [23:14]:LINEVAL + 1= 272;,所以LINEVAL = 271;垂直⽅向尺⼨,多少⾏** [13:6]:VFPD = ⼀帧的数据传输完成之后,过多长时间开始下⼀帧数据的帧同步信号,单位为⾏,理解为1⾏的时间 * 看LCD⼿册tvf = VFPD + 1 = 2;所以VFPD = 1** [5:0]:VSPW = 帧同步信号的脉冲宽度,单位为⾏* 看LCD⼿册tvp = VSPW + 1 =10;所以VSPW = 9*/lcd_regs->lcdcon2 = (1<<24) | (271<<14) | (1<<6) | (9<<0);/** [25:19]:HBPD = ⾏同步信号发出后,经过多少个VCLK,才发送像素的数据,单位为VCLK* 看LCD⼿册thb = HBPD + 1 = 2;所以HBPD=1** [18:8]:HOZVAL + 1 = 480,所以 HOZVAL = 479;⽔平⽅向尺⼨,多少列**[7:0]:HFPD = ⼀⾏的像素数据传输完成之后,经过多长时间,才能发送下⼀个⾏同步信号,单位为VCLK*看LCD⼿册thf = HFPD + 1 = 2;所以HFPD = 1;*/lcd_regs->lcdcon3 = (1<<19) | (479<<8) | (1<<0);/** [7:0]:HSPW = ⾏同步信号的脉冲宽度,单位为VCLK* 看LCD⼿册thp = HSPW + 1 = 41;所以HSPW = 40**/lcd_regs->lcdcon4 = (40<<0);/** [11] :FRM565 = 1;16位模式的格式 R:G:B = 5:6:5* [10] :INVVCLK = 0;VCLK在哪个边沿取数据 = 0表⽰下降沿取数据* [9] :INVVLINE = 1;⾏同步信号是否需要反转= 1需要反转* [8] :INVVFRAME = 1;帧同步信号是否需要反转= 1需要反转* [7] :INVVD = 0; 数据是否需要反转* [6] :INVVDEN = 0; 数据使能信号是否需要反转* [5] :INVPWREN = 0;电源使能信号是否需要反转* [4] :INVLEND = 0;⾏结束信号是否需要反转* [3] :PWREN = 0;电源使能信号,先不使能* [2] :ENLEND = 1;//⾏结束信号先使能* [1:0] :BSWP 、HWSWP = 0 1;字节内部不需要交换,字节间需要交换*/lcd_regs->lcdcon5= (1<<11) | (3<<8) | (1<<2) | (1<<0);/*3.3、显存和调⾊板设置 *//**利⽤dma_alloc_writecombine分配⼀块连续的显存*/s3c_mylcdfb_info->screen_base = dma_alloc_writecombine(NULL,s3c_mylcdfb_info->screen_size,(&(s3c_mylcdfb_info->fix.smem_start)),GFP_KERNEL);//返回虚拟地址if(s3c_mylcdfb_info->screen_base==NULL) //如果显存分配失败,直接返回{printk("undma_alloc_writecombine\n");return1;}/**将显存的地址告诉LCD控制器(物理地址)*/lcd_regs->lcdsaddr1 = (s3c_mylcdfb_info->fix.smem_start >> 1) & (~(3<<30));//起始地址lcd_regs->lcdsaddr2 = ((s3c_mylcdfb_info->fix.smem_start + s3c_mylcdfb_info->screen_size) >> 1) & 0x1fffff;//结束地址lcd_regs->lcdsaddr3 = (480*16/16); /* ⼀⾏的长度(单位: 2字节) *///s3c_lcd->fix.smem_start = xxx; /* 显存的物理地址 *//* 启动LCD */lcd_regs->lcdcon1 |= (1<<0); /* 使能LCD控制器 */lcd_regs->lcdcon5 |= (1<<3); /* 使能LCD本⾝电源 */*gpbdat |= 1; /* 输出⾼电平, 使能背光 *//*4、注册LCD*/register_framebuffer(s3c_mylcdfb_info);printk("register_framebuffer\n");return0;}static void lcd_drv_exit(void){unregister_framebuffer(s3c_mylcdfb_info);lcd_regs->lcdcon1 &= ~(1<<0); /* 关闭LCD本⾝ */*gpbdat &= ~1; /* 关闭背光 */dma_free_writecombine(NULL, s3c_mylcdfb_info->fix.smem_len, s3c_mylcdfb_info->screen_base, s3c_mylcdfb_info->fix.smem_start);iounmap(lcd_regs);iounmap(gpbcon);iounmap(gpccon);iounmap(gpdcon);iounmap(gpgcon);framebuffer_release(s3c_mylcdfb_info);}module_init(lcd_drv_init);module_exit(lcd_drv_exit);MODULE_LICENSE("GPL");2.重新编译内核,去掉默认的LCDmake menuconfig ,进⼊menu菜单重新设置内核参数:进⼊Device Drivers-> Graphics support:<M> S3C2410 LCD framebuffer support //将⾃带的LCD驱动设为模块, 不编进内核中然后make uImage 编译内核make modules 编译模块为什么要编译模块?因为LCD驱动相关的⽂件也没有编进内核,⽽fb_ops⾥的成员fb_fillrect(), fb_copyarea(), fb_imageblit()⽤的都是drivers/video下⾯的3个⽂件,所以需要这3个的.ko模块,如下图所⽰:3.挂载驱动将编译好的LCD驱动模块和drivers/video⾥的3个.ko模块放⼊nfs⽂件系统⽬录中然后烧写内核, 先装载3个/drivers/video下编译好的模块,再来装载LCD驱动模块挂载LCD驱动后, 如下图,可以通过 ls -l /dev/fb* 命令查看已挂载的LCD设备节点:4.测试运⾏测试有两种:echo hello> /dev/tty1 // LCD上便显⽰hello字段cat Makefile>/dev/tty1 // LCD上便显⽰Makeflie⽂件的内容4.1使⽤上节的键盘驱动在LCD终端打印命令⾏vi /etc/inittab //修改inittab, inittab:配置⽂件,⽤于启动init进程时,读取inittab添加->tty1::askfirst:-/bin/sh //将sh进程(命令⾏)输出到tty1⾥,也就是使LCD输出信息然后重启,insmod装载3个/drivers/video下编译好的模块,再来insmod装载LCD驱动模块,tty1设备便有了,就能看到提⽰信息:如下图,我们insmod上⼀节的键盘驱动后,按下enter键,便能在LCD终端上操作linux了从上图可以看到按下enter键,它就启动了⼀个进程号772的-sh进程,如下图发现这个-sh的描述符都指向了tty1:以上内容转载⾃。
linux中编译驱动的方法
在Linux中编译驱动的方法通常涉及以下步骤:
1. 编写驱动代码:首先,您需要编写适用于Linux内核的驱动代码。
这通常是在内核源代码树之外编写的。
驱动代码通常以C语言编写,并遵循内核编程约定。
2. 获取内核源代码:为了编译驱动,您需要获得Linux内核的源代码。
您可以从Linux官方网站或镜像站点下载内核源代码。
3. 配置内核:在编译驱动之前,您需要配置内核以包含您的驱动。
这可以通过运行`make menuconfig`命令来完成。
在配置菜单中,您可以选择要编译的驱动以及相关的内核选项。
4. 编译驱动:一旦您配置了内核并选择了要编译的驱动,您可以使用`make`命令来编译驱动。
这将在内核源代码目录下生成可执行文件或模块文件。
5. 加载和测试驱动:一旦驱动被编译,您可以将其加载到Linux 内核中以进行测试。
您可以使用`insmod`命令将模块加载到内核,然后使用`dmesg`命令检查内核日志以查看驱动是否正确加载。
这些是基本的步骤,但具体的步骤可能会因您的环境和需求而有所不同。
在编译和加载驱动时,请确保您具有适当的权限和知识,因为这可能需要管理员权限,并且错误的操作可能会导致系统不稳定或损坏。
Linux底层驱动开发从入门到精通的学习路线推荐Linux底层驱动开发是一项涉及操作系统核心的技术,对于想要深入了解Linux系统内部工作原理的开发人员来说,是一门重要的技能。
本文将为你推荐一条学习路线,帮助你从入门到精通掌握Linux底层驱动开发。
一、基础知识学习阶段在开始学习Linux底层驱动开发之前,你需要掌握一些基础知识。
以下是你可以参考的学习路线:1.1 Linux操作系统基础学习Linux操作系统的基础知识是理解和使用Linux底层驱动的前提。
可以选择阅读《鸟哥的Linux私房菜》等入门书籍,了解Linux的基本概念、命令行操作等。
1.2 C语言编程C语言是Linux底层驱动开发的主要语言。
建议学习《C Primer Plus》等经典教材,掌握C语言的基本语法和编程技巧。
1.3 Linux系统编程学习Linux系统编程是理解Linux内核和驱动开发的关键。
推荐学习《Linux系统编程手册》等教材,学习Linux系统调用、进程管理等知识。
1.4 数据结构与算法良好的数据结构和算法基础对于优化和设计高效的驱动程序至关重要。
可以学习《算法导论》等经典教材,掌握数据结构和常用算法的原理和实现。
二、Linux内核了解与分析阶段在掌握了基础知识后,你需要进一步了解Linux内核和驱动的工作原理。
以下是你可以参考的学习路线:2.1 Linux内核源码阅读通过阅读Linux内核源码,你可以深入了解Linux的内核机制和实现细节。
可以选择《深入理解Linux内核》等相关书籍,逐步学习Linux内核代码的组织结构和关键部分。
2.2 设备驱动模型了解Linux内核的设备驱动模型对于编写高效且可维护的驱动程序至关重要。
可以学习Linux设备驱动模型的相关文档和教程,例如Linux Device Drivers (LDD)等。
2.3 内核调试与分析工具掌握一些常用的内核调试和分析工具是进行底层驱动开发的必要技能。
C语言嵌入式Linux开发驱动和系统调用在嵌入式系统领域中,C语言是最常用的编程语言之一。
它具有高效性、可移植性和灵活性,使得它成为开发嵌入式Linux驱动和系统调用的理想选择。
本文将详细介绍C语言在嵌入式Linux开发中的应用,包括驱动开发和系统调用的实现。
一、驱动开发1.1 驱动的定义和作用驱动是连接硬件和操作系统的关键组件,它允许操作系统与具体的硬件设备进行通信。
驱动的主要作用是提供对硬件设备的控制、管理和数据传输。
在嵌入式Linux系统中,驱动的开发需要使用C语言来编写。
1.2 驱动的开发流程驱动的开发可以分为以下几个步骤:1)了解硬件设备:首先要对驱动所涉及的硬件设备有一定的了解,包括设备的主要功能和寄存器的操作方式等。
2)驱动代码编写:使用C语言编写驱动代码,根据硬件设备的数据发送和接收过程设计函数和数据结构。
3)编译和链接:将驱动代码编译成可执行文件,并将其链接到操作系统的内核中。
4)加载和卸载:通过调用命令加载和卸载驱动,使其生效或失效。
5)测试和调试:进行驱动功能的测试和调试工作,确保驱动的正确性和稳定性。
1.3 驱动示例:LED驱动以一个简单的LED驱动为例,说明驱动的开发过程:1)定义LED设备的数据结构:创建一个结构体来表示LED设备的相关信息,例如设备的名称、设备的状态等。
2)实现LED控制函数:编写LED控制函数,通过操作硬件寄存器来控制LED的开关。
3)注册驱动:将驱动注册到操作系统的驱动框架中,使其与操作系统进行通信。
4)加载和卸载驱动:通过命令加载和卸载驱动,对LED进行控制。
二、系统调用2.1 系统调用的定义和作用系统调用是用户程序与操作系统之间的接口,它允许用户程序访问操作系统提供的服务和资源。
系统调用的主要作用是提供对底层硬件和操作系统功能的访问。
2.2 系统调用的分类系统调用可以分为以下几类:1)进程控制:如创建、终止和等待进程等。
2)文件操作:如打开、读取和关闭文件等。
如何编写驱动程序编写驱动程序是一项相对复杂的任务,它与硬件交互并与操作系统进行通信。
在这篇文章中,我将提供一个简要的指南,帮助您了解如何编写驱动程序。
驱动程序是操作系统的一部分,用于管理和控制硬件设备。
它们允许操作系统与硬件交互,并提供硬件访问的接口。
驱动程序不仅仅是通过读写硬件寄存器来实现的,还需要处理中断请求、DMA、内存映射和其他底层硬件访问。
以下是编写驱动程序的一般步骤:1.硬件设备的了解:要编写一个驱动程序,首先需要了解所要驱动的硬件设备的工作原理和规范。
这包括它的寄存器布局、通信方式、中断请求等。
也可以查找相关的文档和参考资料。
2.操作系统的了解:每个操作系统都有自己的驱动程序开发框架和API。
要编写驱动程序,必须熟悉所使用的操作系统。
这包括操作系统的内核机制、设备管理、中断处理程序和设备驱动接口等。
3.驱动程序的架构设计:在开始编写驱动程序之前,需要设计一个驱动程序的架构。
这包括确定驱动程序的基本功能、组织结构和接口。
在这一阶段,可以考虑使用合适的设计模式,如观察者模式或策略模式。
4.编写设备初始化代码:设备初始化代码负责初始化硬件设备并确保它在操作系统中正确识别和配置。
这通常包括读写设备寄存器、设置中断请求、设置DMA等。
5.编写设备访问代码:设备访问代码负责实现驱动程序的主要功能,如读写数据、处理中断请求并与操作系统进行通信。
这可能涉及到编写ISR(中断服务例程)处理中断,实现设备驱动接口等。
6.进行驱动程序测试:在编写完驱动程序之后,应该对其进行测试以确保其正确性和稳定性。
可以编写一些测试用例来验证驱动程序是否按预期工作。
7.驱动程序的部署和调试:一旦驱动程序测试通过,就可以将其部署到操作系统中。
在部署过程中,可能需要进行一些调试和优化,以确保驱动程序的性能和可靠性。
可以使用调试工具来帮助定位和修复错误。
编写驱动程序需要一定的硬件和软件知识,并且需要耐心和细心来处理底层问题。
实现一个嵌入式Linux设备驱动程序的大致流程如下:(l)查看原理图,理解设备的工作原理。
(2)定义主设备号。
设备由一个主设备号和一个次设备号来标识。
主设备号唯一标识了设备类型,即设备驱动程序类型,它是块设备表或字符设备表中设备表项的索引。
次设备号仅由设备驱动程序解释,区分被一个设备驱动控制下的某个独立的设备。
alloc_chrdev_region 申请主设备号unregister_chrdev_region 释放主设备号(3)实现初始化函数。
在驱动程序中实现驱动的注册和卸载。
int register_chrdev 注册设备int unregister_chrdev 卸载设备(4)设计所要实现的文件操作,定义file--operations结构。
file_operations的主要成员:struct module *owner: 指向模块自身open:打开设备release:关闭设备read:从设备上读数据write:向设备上写数据ioctl:I/O控制函数llseek:定位读写指针mmap:映射设备空间到进程的地址空间(5)实现所需的文件操作调用,如read,write等。
(6)实现中断服务,并用request--irq向内核注册,中断并不是每个设备驱动所必需的。
(7)编译该驱动程序到内核中,或者用insmod命令加载模块。
(8)测试该设备,编写应用程序,对驱动程序进行测试。
典型字符设备驱动编写框架:1 编写硬件接口函数2 建立文件系统与设备驱动程序间的接口,如:struct file_operations结构体3 注册设备到chrdevfs全局数组中,注册或注销设备可以在任何时候,但一般在模块加载时注册设备,在模块退出时注销设备。
(module_init();module_exit ();)4 以模块方式编译驱动源码,并将其加载到内核中5 创建设备节点,mknode6 编写应用程序访问底层设备。
如何编写Linux设备驱动程序Linux是Unix操作系统的一种变种,在Linux下编写驱动程序的原理和思想完全类似于其他的Unix系统,但它dos或window环境下的驱动程序有很大的区别。
在Linux环境下设计驱动程序,思想简洁,操作方便,功能也很强大,但是支持函数少,只能依赖kernel中的函数,有些常用的操作要自己来编写,而且调试也不方便。
以下的一些文字主要来源于khg,johnsonm的Write linux device driver,Brennan's Guide to Inline Assembly,The Linux A-Z,还有清华BBS上的有关device driver的一些资料。
一、Linux device driver 的概念系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。
设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。
设备驱动程序是内核的一部分,它完成以下的功能: 1。
对设备初始化和释放。
2。
把数据从内核传送到硬件和从硬件读取数据。
3。
读取应用程序传送给设备文件的数据和回送应用程序请求的数据。
4。
检测和处理设备出现的错误。
在Linux操作系统下有三类主要的设备文件类型,一是字符设备,二是块设备,三是网络设备。
字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。
块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。
已经提到,用户进程是通过设备文件来与实际的硬件打交道。
每个设备文件都都有其文件属性(c/b),表示是字符设备还是块设备?另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们。
设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。
最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。
也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作。
如果你的驱动程序陷入死循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck。
读/写时,它首先察看缓冲区的内容,如果缓冲区的数据如何编写Linux操作系统下的设备驱动程序二、实例剖析我们来写一个最简单的字符设备(比如蜂鸣器)驱动程序。
虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理。
把下面的C代码输入机器,你就会获得一个真正的设备驱动程序。
#define __NO_VERSION__#include <linux/modules.h>#include <linux/version.h>char kernel_version [] = UTS_RELEASE;这一段定义了一些版本信息,虽然用处不是很大,但也必不可少。
Johnsonm 说所有的驱动程序的开头都要包含<linux/config.h>,一般来讲最好使用。
由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如open,read,write,close…,注意,不是fopen,fread,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:struct file_operations{int (*seek) (struct inode * ,struct file *,off_t ,int); //文件定位int (*read) (struct inode * ,struct file *,char ,int);int (*write) (struct inode * ,struct file *,off_t ,int);int (*readdir) (struct inode * ,struct file *,struct dirent * ,int); //读取目录int (*select) (struct inode * ,struct file *,int ,select_table *);// I/O端口复用,非阻塞的状态下实现设备的访问int (*ioctl) (struct inode * ,struct file *,unsined int ,unsigned long);//对设备的属性修改int (*mmap) (struct inode * ,struct file *,struct vm_area_struct *);// 内存映射int (*open) (struct inode * ,struct file *);int (*release) (struct inode * ,struct file *);int (*fsync) (struct inode * ,struct file *); //设备的同步信息int (*fasync) (struct inode * ,struct file *,int); //异步int (*check_media_change) (struct inode * ,struct file *); //检测数据是否发生改变int (*revalidate) (dev_t dev); //使设备重新有效}这个结构的每一个成员的名字都对应着一个系统调用。
用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。
这是linux的设备驱动程序工作的基本原理。
既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。
下面就开始写子程序。
#include <linux/types.h>#include <linux/fs.h> //文件系统#include <linux/mm.h> //内存管理#include <linux/errno.h>#include <asm/segment.h> //汇编语言编写unsigned int test_major = 0; //主设备号,自动搜索static int read_test(struct inode *node,struct file *file,char *buf,int count)//读测试,inode *node表示哪一个设备,*file表示文件描述符,*buf表读取时的接口,count表期望读的字节数,static int 返回值—实际读到的数量{int left;if (verify_area(VERIFY_WRITE,buf,count) == -EFAULT )// verify_area,验证某一个buf中的数据是否有效return -EFAULT;for(left = count ; left > 0 ; left--){__put_user(1,buf,1); //从内核空间将数据拷贝到用户空间去,将”1”依次放到用户空间的buf中,每次放的大小是一个字节buf++;}return count; //返回读的数据数}这个函数是为read调用准备的。
当调用read时,read_test()被调用,它把用户的缓冲区全部写1。
buf 是read调用的一个参数。
它是用户进程空间的一个地址。
但是在read_test被调用时,系统进入核心态。
所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据。
另外还有很多类似功能的函数。
请参考,在向用户空间拷贝数据之前,必须验证buf 是否可用。
这就用到函数verify_area。
static int write_tibet(struct inode *inode,struct file *file,const char *buf,int count) {return count;}// *inode表入口节点,*file表设备描述符static int open_tibet(struct inode *inode,struct file *file ){MOD_INC_USE_COUNT; //宏定义,注册一个驱动之后,模块数自动+1return 0;}static void release_tibet(struct inode *inode,struct file *file ){MOD_DEC_USE_COUNT; // 模块数自动减1}这几个函数都是空操作。
实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。
struct file_operations test_fops = {NULL,read_test,write_test,NULL,/* test_readdir */NULL,NULL,/* test_ioctl */NULL,/* test_mmap */open_test,release_test,NULL,/* test_fsync */NULL,/* test_fasync *//* nothing more,fill with NULLs */};设备驱动程序的主体可以说是写好了。
现在要把驱动程序嵌入内核。
驱动程序可以按照两种方式编译。
一种是编译进kernel,另一种是编译成模块(modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。
int init_module(void) //注册方式{int result;result = register_chrdev(0,"test",&test_fops);//注册字符型设备到内核中去,“0”表是自动根据设备节点里的主设备号来获取它的设备号;"test"表示你注册的设备名;&test_fops为注册的接口if (result < 0) {printk(KERN_INFO "test: can't get major number\n"); //在内核打印信息必须用printk;而print f只能在用户空间用return result;}if (test_major == 0) test_major = result; /* dynamic *///由内核分配一个设备号给驱动程序,获取设备的主设备号return 0;}在用insmod命令将编译好的模块调入内存时,init_module 函数被调用。