版权声明
本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。
Copyright (C) 2021 凌云物网智科实验室·郭工
Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292
3.1. Linux驱动开发入门
Linux系统下的程序开发一般分为两种: 一种是应用程序开发,一种是内核级驱动程序开发,这两种开发种类对应Linux的两种状态,分别是用户态和内核态。当我们在应用程序空间编写一个打印“Hello World”字符串的程序时,在调用 printf("Hello World")
之前的所有代码都运行在用户态。而当C语言库函数printf() 要开始往LCD显示器上打印”Hello World” 字符串时,它将会通过调用 write()
系统调用来实现。而该系统调用将会让该进程从 用户态 切换到 内核态 来执行,此时Linux内核中的代码将会调用LCD驱动提供的相应接口函数,把该字符串输出到LCD显示屏上。在完成这些显示工作后,write()
系统调用将会返回,此时该进程将会从 内核态 切回到 用户态 继续运行。
由此可知:
进程从 用户态 切换到 内核态 一般是由 系统调用(System Call) 来实现的;
系统调用返回时,进程将会从 内核态 切换到 用户态;
在Linux系统下,我们可以使用 time 命令查看一个进程(程序) 分别在 用户态 和 内核态 运行了多长时间。
guowenxue@7eeebdd3d42f:~$ time printf "Hello World\n"
Hello World
real 0m0.000s
user 0m0.000s
sys 0m0.000s
Linux设备驱动程序在 Linux 内核里扮演着特殊的角色. 它们是截然不同的”黑盒子”,实现了对硬件的配置和控制,并对其进行进一步的抽象,为应用层软件操作硬件提供了统一的接口函数。不论硬件的具体形式如何,linux驱动都将其映射成一个设备文件(存放在Linux系统的 /dev 路径下,譬如早期的Linux系统下LCD对应的设备文件就是 /dev/fb0),应用程序空间只需要调用open()、read()、write()、ioctl()等这些标准的系统调用API,就可以操作实际的硬件了。
本篇将从hello world开始,简要介绍驱动的基本结构及其使用方法,接下来将会进一步讲解设备上的各种设备驱动的编写,让大家对Linux驱动有一个基本的认识。
3.1.1 Linux内核功能介绍
在 Linux 系统中, 几个并发的进程用来处理不同的任务. 每个进程都需要向操作系统请求系统资源, 如CPU、内存、网络连接或者一些其它的资源, 而这些功能都是通过系统调用来完成的. 这样,Linux内核可以看作是一个大块的可执行文件, 负责处理所有这样的请求。对于Linux内核而言,其主要功能职责有这么几个:
进程管理: Linux内核负责创建和销毁进程(应用程序空间调用 fork() 后), 并处理它们与外部世界的联系(输入和输出),此外它还实现了不同进程间的各种通信方式,如管道、信号、信号量等,这些对操作系统来说是最基本的. 此外,进程调度程序则执行相应的进程调度策略,以确保各个进程可以公平地访问CPU,并实现实现了多个进程在一个单核或多核 CPU 上的抽象。
内存管理: 计算机的内存是主要的资源,,处理它所用的策略对系统性能是至关重要的,它允许多个进程安全地共享机器的主内存系统。内存管理器主要使用虚拟内存管理机制,它为每个进程都建立了一个独立的4GB虚拟地址空间,从而保证每个进程都有足够的内存运行并互不影响,而我们的物理主机实际上可能只有1GB甚至更少的内存。内核的不同部分与内存管理子系统通过一套函数调用交互, 从简单的 malloc/free 到更多、更复杂的功能。
文件系统: Unix 系统的成功很大程度上基于文件系统的概念,几乎 Unix 中的任何东西都可看作一个文件. 内核在非结构化的硬件之上建立了一个结构化的文件系统, 结果是文件的抽象非常多地在整个系统中应用. 另外, Linux系统通过 VFS(Virtual File System) 支持多个文件系统类型。例如, 磁盘可被格式化成 Linux 的 ext4 文件系统, 也可以使用Windows的 FAT32 文件系统,或者其他几个文件系统。
设备管理: 除了处理器, 内存和非常少的别的实体之外, 几乎每个系统操作最终都会映射到一个物理设备上,如使用vim编辑文件是将会操作到屏幕、鼠标、磁盘等。而任何设备控制操作都需要由其相应的驱动程序来实现,如显示器需要显卡的驱动才能驱动显示、鼠标需要鼠标的驱动才能工作、磁盘需要磁盘驱动才能工作。如何在Linux系统下编写相应的设备驱动,是我们这里重点讨论的东西。在Linux系统下,除几乎所有的字符设备和块设备都会在 /dev 路径下有一个设备文件与其相对应。
网络功能: Linux系统之所以在嵌入式领域、服务器领域由非常非常广泛的应用,正是由于它出色的网络通信功能。在Linux内核里,它实现了常见的网络协议栈(TCP/IP),以及对底层网络设备的控制,并对上层提供统一的网络socket编程接口。需要注意的是,网卡设备在 /dev 路径下并没有相应的设备文件,它都是通过 socket() 来管理的。事实上我们常用的 ifconfig、route 等命令也是由 socket 系统调用实现的,因为网卡设备并没有具体的设备文件。
3.1.2 Linux设备驱动分类
Linux 系统根据设备特点,将设备驱动程序大致分为下面三大类:
1.字符设备驱动: 这类设备在进行数据读取操作时是以字节为单位进行的,对于这种字节流的设备叫做字符设备。典型的如串口、Led、LCD、蜂鸣器、SPI、触摸屏等驱动,都属于字符设备驱动的范畴。需要注意的是,Linux系统中大部分的驱动程序都是属于字符设备驱动。我们可以使用命令 ls -l /dev/ | grep ^c
查看当前系统下的字符设备。
2.块设备驱动: 块设备驱动是相对于字符设备驱动而定义的,因为块设备被软件操作时,是以块或扇区(block/sector)为单位进行操作的(块指的是多个字节组成一个块)。块设备大多指的都是各种存储类类设备,比如硬盘、U盘、SD卡、eMMC、NandFlash等等。这类设备在Linux下使用时,一般都需要进行分区、格式化(文件系统)、挂载(mount)起来使用。我们可以使用命令 ls -l /dev/ | grep ^b
查看当前系统下的块设备。
3.网络设备驱动: 专门针对网络设备而设计的一种驱动,不管是有线还是无线网络,都属于网络设备驱动。对于 Linux下的字符设备和块设备在 /dev 路径下都会有一个设备节点与其相对应,但网络设备并不存在这样的设备节点。如果想要查看网络设备的信息,应该使用 ifconfig 命令来查看,而如果要使用网络设备进行通信,则应该使用 socket 编程API来实现。
另外需要注意的是,有些设备是可以属于多种设备驱动类型,比如 USB WIFI设备,其使用 USB 接口,所以属于字符设备(USB驱动),但是其又能上网,所以也属于网络设备(网卡驱动)。而U盘也是使用的USB接口则属于字符设备,它又能做存储所以又是块设备。事实上,USB只是设备与CPU之间通信的通信协议,这样USB设备则会在Linux内核中根据其功能模拟实现成不同的设备驱动(USB鼠标键盘模拟成HID字符设备,USB网卡模拟成网络设备,U盘模拟成块设备)。
3.1.3 内核驱动开发注意事项
大多数程序员致力于应用程序的开发,少数程序员则致力于内核及驱动程序的开发。相对于应用程序的开发,内核及驱动程序的开发有很大的不同。最重要的差异包括以下几点:
内核及驱动程序开发时不能使用C库提供的函数,如printf()等。因为C库是在应用程序空间中编程使用的,它里面很多函数需要调用Linux内核中的系统调用来实现的,如printf() 将会调用内核的 write()系统调用。这样,很显然我们在编写Linux驱动程序时不能调用printf()函数,而应该使用Linux内核里实现的 printk() 函数。
Linux应用程序空间中的每个进程都有受保护的4GB的虚拟地址空间,这样我们在应用程序编程出现指针错误时,只会导致该进程退出(通常会抛Segmentation Fault),并不会导致系统或其它进程奔溃。而Linux内核驱动编程时出现指针错误将可能会导致整个Linux系统死机(通常会抛Kernel Panic),所以Linux内核驱动编程要异常小心。
内核里只有一个很小的定长堆栈,这样在驱动编程时不能像应用程序空间一样随意开辟一段大的存储空间,另外在内核里动态分配的内存使用完成之后务必要要记得释放。
Linux内核空间不支持浮点运算,这样在驱动程序开发时使用浮点数将会很难,应该使用整型数。譬如我们在写温湿度传感器驱动时,往往不会直接返回一个浮点类型的值。
内核及驱动程序开发时必须使用GNU C,因为Linux操作系统从一开始就使用的是GNU C,虽然也可以使用其他的编译工具,但是需要对以前的代码做大量的修改。
内核支持异步终端、抢占和SMP,因此内核及驱动程序开发时必须时刻注意同步和并发。
内核及驱动程序开发要考虑可移植性,因为对于不同的平台,驱动程序是不兼容的。
3.1.4 内核驱动开发基本原则
作为一个程序员, 你能够对你的驱动作出你自己的选择, 并且在所需的编程时间和结果的灵活性之间, 选择一个可接受的平衡. 但在做驱动开发时,我们应该遵循一个基本的原则:***驱动程序的角色应该是提供机制, 而不是策略。***机制和策略的区分是 Unix/Linux 系统设计背后最好的哲学,这也是类Unix系统的应用程序接口这么多年来保持统一、稳定、不变的核心原因。
那什么是机制和策略呢?这里以Led灯的驱动为例,对于Led驱动而言,我们应该提供Led灯操作的基本功能,如点亮Led、熄灭Led,那这些就是机制。而在某个项目中有个需求要让Led灯亮10s后再熄灭,这个就是策略。这样,我们在Led驱动实现中,应该只提供Led的点亮和熄灭操作函数(机制),而不应该提供把Led亮10s然后再熄灭的功能函数(策略)。
之所以在写驱动时,需要把机制和策略区分开来,这是为了让我们的驱动能够具备更大的可扩展性和兼容性。试想一下,如果我们在Led驱动中实现了亮10s后再熄灭的“策略”,那如果今后的需求变更需要亮15s后再熄灭,此时我们需要重新修改驱动源码、编译驱动内核并升级Linux系统。而频繁升级Linux内核或系统,这可是用户不能接受的,并且一旦Linux内核升级失败会导致系统不能启动,出现灾难性的后果。
3.1.5 Linux源码及版权问题
Linux 是以 GNU 通用公共版权( GPL )的版本 2 作为许可的, 它来自自由软件基金的 GNU 项目. GPL 允许任何人重发布, 甚至是销售, GPL 涵盖的产品, 只要接收方对源码能存取并且能够行使同样的权力. 另外, 任何源自使用 GPL 产品的软件产品, 如果它是完全的重新发布, 必须置于 GPL 之下发行.
这样一个许可的主要目的是允许知识的增长, 通过同意每个人去任意修改程序; 同时, 销售软件给公众的人仍然可以做他们的工作. 尽管这是一个简单的目标, 关于 GPL 和它的使用存在着从未结束的讨论. 如果你想阅读这个许可证, 你能够在你的系统中几个地方发现它, 包括你的内核源码树的目录中的 COPYING 文件
如果你想你的代码进入主流内核, 或者如果你的代码需要对内核的补丁, 你在发行代码时, 必须立刻使用一个 GPL 兼容的许可. 尽管个人使用你的改变不需要强加 GPL, 如果你发布你的代码, 你必须包含你的代码到发布里面 – 要求你的软件包的人必须被允许任意重建二进制的内容.
最后,Linux内核完全是免费、开源的代码,大家可以随意下载、使用、阅读学习Linux内核源码,其官方站点地址为 https://kernel.org/ 。如果想要深入掌握Linux驱动开发,在完成接下来的驱动开发工作以外,我们还需要阅读大量的Linux内核源码中的驱动文件,这样才能对Linux内核各个子系统及驱动框架有更深入的理解和认识。