Linux bsp驱动学习一:驱动开发入门

设备驱动程序是计算机硬件与应用程序的接口,是软件系统与硬件系统沟通的桥梁。 如果没有设备驱动程序,那么硬件设备就只是一堆废铁,没有什么功能。

一、Linux 操作系统与驱动的关系

​ Linux系统下的程序开发一般分为两种: 一种是应用程序开发,一种是内核级驱动程序开发,这两种开发种类对应Linux的两种状态,分别是用户态和内核态。当我们在应用程序空间编写一个打印“Hello World”字符串的程序时,在调用 printf("Hello World") 之前的所有代码都运行在用户态。而当C语言库函数printf() 要开始往LCD显示器上打印”Hello World” 字符串时,它将会通过调用 write()系统调用来实现。而该系统调用将会让该进程从 用户态 切换到 内核态 来执行,此时Linux内核中的代码将会调用LCD驱动提供的相应接口函数,把该字符串输出到LCD显示屏上。在完成这些显示工作后,write() 系统调用将会返回,此时该进程将会从 内核态 切回到 用户态 继续运行。

kernel_userspace

由此可知:

  • 进程从 用户态 切换到 内核态 一般是由 系统调用(System Call) 来实现的;
  • 系统调用返回时,进程将会从 内核态 切换到 用户态

在Linux系统下,我们可以使用 time 命令查看一个进程(程序) 分别在 用户态内核态 运行了多长时间。

Linux设备驱动程序在 Linux 内核里扮演着特殊的角色. 它们是截然不同的”黑盒子”,实现了对硬件的配置和控制,并对其进行进一步的抽象,为应用层软件操作硬件提供了统一的接口函数。不论硬件的具体形式如何,linux驱动都将其映射成一个设备文件(存放在Linux系统的 /dev 路径下,譬如早期的Linux系统下LCD对应的设备文件就是 /dev/fb0),应用程序空间只需要调用open()、read()、write()、ioctl()等这些标准的系统调用API,就可以操作实际的硬件了。

driver_model

二、Linux驱动程序开发

​ Linux 驱动程序的开发与应用程序的开发有很大的差别。这些差别导致了编写 Linux 设备驱动程序与编写应用程序有本质的区别,所以对于应用程序的设计技巧很难直接应用 在驱动程序的开发上。最经典的例子是应用程序如果错误可以通过 try catch 等方式,避免 程序的崩溃,驱动程序则没有这么好的处理方式。

1、用户态和内核态

​ Linux 操作系统分为用户态和内核态。用户态处理上层的软件工作。内核态用来管理用户态的程序,完成用户态请求的工作。驱动程序与底层的硬件交互,所以工作在内核态。

​ 简单来说,内核态大部分时间在完成与硬件的交互,相对于内核态,用户态则自由得多。

​ 另一方面,Linux 操作系统分为两个状态的原因主要是,为应用程序提供一个统一的计算机硬件抽象。工作在用户态的应用程序完全可以不考虑底层的硬件操作,这些操作由内核态程序来完成。这些内核态程序大部分是设备驱动程序。一个好的操作系统的驱动程 序对用户态应用程序应该是透明的,也就是说,应用程序可以在不了解硬件工作原理的情况下,很好地操作硬件设备,同时不会使硬件设备进入非法状态。

​ 一个值得注意的问题是,工作在用户态的应用程序不能因为一些错误而破坏内核态的程序。现代处理器已经充分考虑了这个问题。处理器提供了一些指令,分为特权指令和普通指令。特权指令只有在内核态下才能使用;普通指令既可以在内核态使用,也可以在用户态使用。通过这种限制,用户态程序就不能执行只有在内核态才能执行的程序了,从而起到保护的作用。

​ 另一个值得注意的问题是,用户态和内核态是可以互相转换的。每当应用程序执行系统调用或者被硬件中断挂起时,Linux 操作系统都会从用户态切换到内核态。当系统调用完成或者中断处理完成后,操作系统会从内核态返回用户态,继续执行应用程序。

2、模块机制

​ 模块是可以在运行时加入内核的代码,这是 Linux 一个很好的特性。这个特性使内核可以很容易地扩大或缩小,一方面扩大内核可以增加内核的功能,另一方面缩小内核可以减小内核的大小。

​ Linux 内核支持很多种模块,驱动程序就是其中最重要的一种,甚至文件系统也可以写成一个模块,然后加入内核中。每一个模块由编译好的目标代码组成,可以使用 insmod(insert module 的缩写)命令将模块加入正在运行的内核,也可以使用 rmmod(remove module的缩写)命令将一个未使用的模块从内核中删除。

​ 试图删除一个正在使用的模块,将是不允许的。

​ 模块在内核启动时装载称为静态装载,在内核已经运行时装载称为动态装载。模块可以扩充内核所期望的任何功能,但通常用于实现设备驱动程序。

三、内核驱动开发注意事项

​ 大多数程序员致力于应用程序的开发,少数程序员则致力于内核及驱动程序的开发。相对于应用程序的开发,内核及驱动程序的开发有很大的不同。最重要的差异包括以下几点:

  • 内核及驱动程序开发时不能访问C库,因为C库是使用内核中的系统调用来实现的,而且是在用户空间实现的。所以在编写Linux驱动程序时不能调用printf()函数,而应该使用Linux内核里实现的 printk() 函数,它可以看作是Linux内核里的printf()函数实现。
  • Linux应用程序空间中的每个进程都有受保护的4GB的虚拟地址空间,这样我们在应用程序编程出现指针错误时,只会导致该进程退出(通常会抛Segmentation Fault),并不会导致系统或其它进程奔溃。而Linux内核驱动编程时出现指针错误将可能会导致整个Linux系统死机(通常会抛Kernel Panic),所以Linux内核驱动编程要异常小心。
  • 内核里只有一个很小的定长堆栈,这样在驱动编程时不能像应用程序空间一样随意开辟一段大的存储空间,另外在内核里动态分配的内存使用完成之后务必要要记得释放。
  • Linux内核空间不支持浮点运算,这样在驱动程序开发时使用浮点数将会很难,应该使用整型数。譬如我们在写温湿度传感器驱动时,往往不会直接返回一个浮点类型的值。
  • 内核及驱动程序开发时必须使用GNU C,因为Linux操作系统从一开始就使用的是GNU C,虽然也可以使用其他的编译工具,但是需要对以前的代码做大量的修改。
  • 内核支持异步终端、抢占和SMP,因此内核及驱动程序开发时必须时刻注意同步和并发。
  • 内核及驱动程序开发要考虑可移植性,因为对于不同的平台,驱动程序是不兼容的。

四、 内核驱动开发基本原则

​ 作为一个程序员, 你能够对你的驱动作出你自己的选择, 并且在所需的编程时间和结果的灵活性之间, 选择一个可接受的平衡. 但在做驱动开发时,我们应该遵循一个基本的原则:驱动程序的角色应该是提供机制, 而不是策略。机制和策略的区分是 Unix/Linux 系统设计背后最好的哲学,这也是类Unix系统的应用程序接口这么多年来保持统一、稳定、不变的核心原因。

​ 那什么是机制和策略呢?这里以Led灯的驱动为例,对于Led驱动而言,我们应该提供Led灯操作的基本功能,如点亮Led、熄灭Led,那这些就是机制。而在某个项目中有个需求要让Led灯亮10s后再熄灭,这个就是策略。这样,我们在Led驱动实现中,应该只提供Led的点亮和熄灭操作函数(机制),而不应该提供把Led亮10s然后再熄灭的功能函数(策略)。

​ 之所以在写驱动时,需要把机制和策略区分开来,这是为了让我们的驱动能够具备更大的可扩展性和兼容性。试想一下,如果我们在Led驱动中实现了亮10s后再熄灭的“策略”,那如果今后的需求变更需要亮15s后再熄灭,此时我们需要重新修改驱动源码、编译驱动内核并升级Linux系统。而频繁升级Linux内核或系统,这可是用户不能接受的,并且一旦Linux内核升级失败会导致系统不能启动,出现灾难性的后果。

五、Linux源码及版权问题

​ Linux 是以 GNU 通用公共版权( GPL )的版本 2 作为许可的, 它来自自由软件基金的 GNU 项目. GPL 允许任何人重发布, 甚至是销售, GPL 涵盖的产品, 只要接收方对源码能存取并且能够行使同样的权力. 另外, 任何源自使用 GPL 产品的软件产品, 如果它是完全的重新发布, 必须置于 GPL 之下发行.

​ 这样一个许可的主要目的是允许知识的增长, 通过同意每个人去任意修改程序; 同时, 销售软件给公众的人仍然可以做他们的工作. 尽管这是一个简单的目标, 关于 GPL 和它的使用存在着从未结束的讨论. 如果你想阅读这个许可证, 你能够在你的系统中几个地方发现它, 包括你的内核源码树的目录中的 COPYING 文件

​ 如果你想你的代码进入主流内核, 或者如果你的代码需要对内核的补丁, 你在发行代码时, 必须立刻使用一个 GPL 兼容的许可. 尽管个人使用你的改变不需要强加 GPL, 如果你发布你的代码, 你必须包含你的代码到发布里面 – 要求你的软件包的人必须被允许任意重建二进制的内容.

​ 最后,Linux内核完全是免费、开源的代码,大家可以随意下载、使用、阅读学习Linux内核源码,其官方站点地址为 https://kernel.org/ 。如果想要深入掌握Linux驱动开发,在完成接下来的驱动开发工作以外,我们还需要阅读大量的Linux内核源码中的驱动文件,这样才能对Linux内核各个子系统及驱动框架有更深入的理解和认识。