linux设备驱动(一)

基本概念

什么是设备驱动

计算机的运行,需要软件和硬件的合作,没有软件的硬件,是一堆废铁,没有硬件的软件,是空中楼阁,软件运行在硬件基础之上。

软件又分为系统软件和应用软件,系统软件屏蔽了底层硬件的差异,向应用层提供统一的接口,因而应用层的程序员,不需要知道不同网卡、硬盘等硬件之间有什么差异,只需要调用相同的读写函数,即可完成对设备的读写操作。这个屏蔽具体硬件差异过程的重任,就落到了设备驱动的头上。设备驱动程序,仅仅是系统软件的一部分。

设备驱动是底层硬件和上层应用之间的纽带,设备驱动最通俗的解释是:驱使硬件设备行动。设备驱动与底层硬件直接打交道,完成寄存器的读写等、中断处理等等操作。

无操作系统下的驱动

计算机的运转不一定需要操作系统,目前stm8,stm32等主流单片机都可以直接跑裸机程序,一般认为只需要完成main函数和中断服务函数即可(当然IDE也帮我们完成了很多其它的底层工作)。这样简单的应用场景,并不需要操作系统提供的多任务调度,内存管理,文件系统等功能。

虽然没有操作系统,但是并不意味着没有驱动程序。一般情况下,每一个硬件设备,都对应了一个.c和.h文件,例如EEPROM芯片AT24C04,在程序中肯定会对该芯片多次的进行读写操作,为了方便,肯定会将这些重复使用的代码实现为函数,加入到整个工程之中,这些代码,也可以称之为驱动。只不过这些驱动程序的接口直接提供给应用层,应用层没有跨越任何层次,就直接访问到了底层硬件。

有操作系统下的驱动

驱动程序属于内核的一部分,内核是操作系统最核心的部分,它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性,因此驱动程序要按照内核的规定的格式和提供的接口来写。

在有操作系统的情况下,驱动程序其实是连接硬件和内核的一个桥梁,操作系统的存在,让驱动程序增加了很多代码,操作系统就是通过给驱动程序编写者制造麻烦,达到方便应用开发者的目的。

linux设备分类

驱动设备针对的存储器和外设,而不是针对cpu内核,linux系统将存储器和外设分为三大类:

  • 字符设备
  • 块设备
  • 网络设备

字符设备

字符设备指那些必须以串行顺序依次进行访问(字符流)的设备,如键盘、鼠标、串口等,系统虚拟出一个文件,对该文件调用读写函数(read、write)即可完成对字符设备的操作

块设备

系统可以随机访问快设备,以块为单位进行操作,如硬盘、FLASH、CD-ROM等,系统虚拟出一个文件,或者在块设备上建立文件系统,对这些文件调用读写函数(read、write)即可完成对块设备的操作

网络设备

网络设备通常指有线网卡、无线网卡等,网络设备面向数据包的接收和发送而设计,与字符设备和块设备不同,网络通信主要依靠套接字接口

内核的组成

源码目录结构

Linux 内核源代码包含如下目录。

  • arch :包含和硬件体系结构相关的代码,每种平台占一个相应的目录,如 i386、arm、
    arm64、powerpc、mips 等。Linux 内核目前已经支持 30 种左右的体系结构。在 arch
    目录下,存放的是各个平台以及各个平台的芯片对 Linux 内核进程调度、内存管理、
    中断等的支持,以及每个具体的 SoC 和电路板的板级支持代码。
  • block:块设备驱动程序 I/O 调度。
  • crypto:常用加密和散列算法(如 AES、SHA 等),还有一些压缩和 CRC 校验算法。
  • documentation:内核各部分的通用解释和注释。
  • drivers :设备驱动程序,每个不同的驱动占用一个子目录,如 char、block、net、
    mtd、i2c 等。
  • fs:所支持的各种文件系统,如 EXT、FAT、NTFS、JFFS2 等。
  • include:头文件,与系统相关的头文件放置在 include/linux 子目录下。
  • init:内核初始化代码。著名的 start_kernel() 就位于 init/main.c 文件中。
  • ipc:进程间通信的代码。
  • kernel :内核最核心的部分,包括进程调度、定时器等,而和平台相关的一部分代码
    放在 arch/*/kernel 目录下。
  • ib:库文件代码。
  • mm:内存管理代码,和平台相关的一部分代码放在 arch/*/mm 目录下。
  • net:网络相关代码,实现各种常见的网络协议。
  • scripts:用于配置内核的脚本文件。
  • security:主要是一个 SELinux 的模块。
  • sound:ALSA、OSS 音频设备的驱动核心代码和常用设备驱动。
  • usr:实现用于打包和压缩的 cpio 等。
  • include:内核 API 级别头文件。

内核主要组成部分

内核

进程调度SCHED

进程调度控制系统使得多个进程在CPU的中微观上分时执行,宏观上并发执行
进程整体上可以分为五种状态:

  • 就绪态:资源准备就绪,等待系统调用该进程
  • 执行态:当前CPU正在执行该进程
  • 睡眠态:等待其它进程释放资源
  • 暂停态:停止执行,等待某种信号
  • 僵尸态:进程已死,资源未释放

内存管理MM

内存管理的主要作用是控制多个进程安全地共享主内存区域。当CPU提供内存管理单元(MMU)时,Linux内存管理对每个进程提供虚拟地址到物理地址的转换

每个进程拥有虚拟的4G内存空间,其中3G是用户空间,是属于进程私有的,1G是内核空间,是所有进程共享的

虚拟文件系统VFS

用户通过虚拟文件系统,对文件进行操作,即可达到访问硬件的目的,虚拟文件系统隐藏了硬件差异,向上为用户提供统一的接口,向下调用驱动程序实现的file_operations结构体中的成员函数
文件系统

网络接口NET

网络接口提供了对各种网络标准的存取和各种网络硬件的支持。在 Linux中网络接口可分为网络协议和网络驱动程序,网络协议部分负责实现每一种可能的网络传输协议,网络设备驱动程序负责与硬件设备通信,每一种可能的硬件设备都有相应的设备驱动程序。
网络接口

进程间通信IPC

Linux支持进程间通信的多种机制,包括信号量、共享内存、消息队列、管道、套接字等。利用这些机制,可以实现进程间的同步、互斥和消息传递。

内核代码命名风格

与windows下流行的编程命名风格不同,linux社区形成了自己的一套风格

#define PI 3.1415926
int min_value, max_value;
void send_data(void);
  • 宏:大写
  • 变量:全部小写,单词之间以下划线分隔
  • 函数:同变量
  • 缩进:tab键
  • 括号{}:
    1.if、for、switch、while、else、else if { 不会另起一行
    2.函数的 { 需要另起一行
    3.只有一行代码,不要用括号{}
  • switch:switch、case、default关键字左对齐

GNU C对ANSI C的拓展

Linux使用的C编译器是GNU C编译器,对标准C进行了一系列拓展,增强C的功能

变长数组

GNU C可以使用0长度数组,也可以使用变量定义数组长度

struct var_data {
int len;
char data[0];
};

int n=10;
int array[n];

其实也就是C99中新增加的伸缩数组和变长数组的概念

伸缩的意思是,比如定义一个结构体,可以让其成员拥有不固定的长度,因此用sizeof去求该结构体类型,会忽略掉不固定的部分,而实际定义的时候,一般需要用malloc进行内存分配

变长数组不是指数组的长度在程序运行期间可变,而是指定义时可以利用变量,对长度进行说明,一旦定义了一个数组,其长度就不能变了

case

GNU C 支持 case x…y 这样的语法,区间 [x,y] 中的数都会满足这个 case 的条件

switch (ch) {
case '0'... '9': c -= '0';
break;
case 'a'... 'f': c -= 'a' - 10;
break;
case 'A'... 'F': c -= 'A' - 10;
break;
}

语句表达式

GNU C 把包含在括号中的复合语句看成是一个表达式,称为语句表达式,它可以出现在任何允许表达式的地方。我们可以在语句表达式中使用原本只能在复合语句中使用的循环、局部变量等,

typeof关键字

typeof(x) 语句可以获得 x 的类型,可以方便一些宏的实现,例如求两数中较大的值
# define max(a,b) ...
具体实现中,需要先判断传入数据的类型,此处可以通过typeof进行判断

标号元素

C99标准已经增加了标号元素的使用,也就是数组、结构体等初始化过程不需要按顺序来,可以随意指定,例如:

int a[10] = {
    [1] = 2,
    [3] = 5,
    0,
}

在Linux内核代码中,推荐以下格式进行初始化:

struct file_operations ext2_file_operations = {
    .llseek       = generic_file_llseek,
    .read        = generic_file_read,
    ...
};

特殊属性说明

GNU C 允许声明函数、变量和类型的特殊属性,以便手动优化代码和定制代码检查的方法。要指定一个声明的属性,只需要在声明后添加_ _attribute_ _ (( ATTRIBUTE ))

其中 ATTRIBUTE 为属性说明,如果存在多个属性,则以逗号分隔。GNU C 支持 noreturn、format、section、aligned、packed 等十多个属性

  • noreturn 属性作用于函数,表示该函数从不返回。这会让编译器优化代码,并消除不必要的警告信息
  • format 属性也用于函数,表示该函数使用 printf、scanf 或 strftime 风格的参数,指定format 属性可以让编译器根据格式串检查参数类型
  • unused 属性作用于函数和变量,表示该函数或变量可能不会用到,这个属性可以避免编译器产生警告信息
  • aligned 属性用于变量、结构体或联合体,指定变量、结构体或联合体的对齐方式,以字节为单位
  • packed 属性作用于变量和类型,用于变量或结构体成员时表示使用最小可能的对齐,用于枚举、结构体或联合体类型时表示该类型使用最小的内存

内建函数

内建函数是编译器内部实现的函数,这些函数可以直接使用,无须声明或者添加头文件,不属于标准库函数的内建函数通常以_ _builtin开头,还有一小部分内建函数与标准库函数同名,不过编译器做了优化或者重构。

工具链

最方便的就是直接下载已经编译好的工具链,目前最常用的是Linaro提供的,可以在下面的网址进行下载

http://www.linaro.org/downloads/

Linaro 是 ARM Linux 领域中最著名最具技术成就的开源组织,其会员包括 ARM、Broadcom、Samsung、TI、Qualcomm 等,国内的海思、中兴、全志和中国台湾的 MediaTek 也是它的会员

工具链包含了很多程序,例如arm-linux-gnueabihf-gcc
下面介绍一下常用的几种

  • gcc:编译C源码
  • g++:编译C++源码
  • gdb:调试
  • strip:删除符号表和调试信息,缩减可执行文件的体积
  • objdump:反汇编
  • ld:链接器
  • gprof:检查函数被调用的次数和时间
  • nm:显示符号信息
  • readelf:查看ELF格式文件信息
  • addr2line:将指令的地址和可执行映像转换为文件名、函数名和源代码行数的工具

环境搭建

练习驱动程序开发,最好的方式是虚拟机+qemu,不需要额外购买开发板,具体搭建的过程可以参考我之前的博客

https://blog.csdn.net/whitefish520/article/details/103850361

猜你喜欢

转载自blog.csdn.net/whitefish520/article/details/103892693
今日推荐