赵工的个人空间


专业技术部分转网页计算转业余爱好部分


 单片机与嵌入式

首页 > 专业技术 > 单片机与嵌入式 > 嵌入式Linux编程
嵌入式Linux编程
  1. 嵌入式Linux的介绍
  2. 搭建嵌入式Linux开发平台
  3. 嵌入式Linux交叉编译
  4. 嵌入式Linux编程基础
  5. Linux设备驱动程序基础
  6. 嵌入式Linux设备驱动的开发
  7. 嵌入式Web服务器
  8. 公共网关接口CGI
  9. SQLite数据库
  10. 网络分析工具Wireshark
  11. Qt编程
  12. Android应用开发

1. 嵌入式Linux的介绍:

嵌入式Linux是对标准Linux经过小型化剪裁处理之后,固化在容量只有几千字节或几兆字节的存储芯片或者单片机中。

1) 嵌入式Linux的优势:

嵌入式Linux版权免费,由全世界的自由软件开发者提供技术支持,性能优良,软件移植容易,代码开放,有很多应用软件支持,有很多公开的代码可以参考和移植。Linux适用于多种硬件平台,包括x86、ARM、MIPS、ALPHA、PowerPC等多种体系架构,目前已经移植到多个平台,几乎可以运行在所有流行的CPU上。嵌入式Linux利用GNU的gcc作为编译器,为开发者提供了完整的工具链,用gdb、kgdb、xgdb作调试工具。Linux支持标准的Internet网络协议,使其很容易移植到嵌入式系统中,并支持多种文件系统。
嵌入式Linux有很多版本:
  ·  uCLinux:源码开放的嵌入式Linux,针对目标管理器没有存储管理的单MMU而设计,运行稳定,具有良好移植性和网络功能,支持多种文件系统,提供丰富API。
  ·  RT-Linux:美国墨西哥理工学院开发的实时操作系统。
  ·  Embedix:根据嵌入式要求重新设计的Linux发行版本,提供超过25种Linux系统服务,包括Web服务器,并推出开发调试工具包、基于图形界面的浏览器等。
  ·  XLinux:采用unicode,使Linux内核不仅与标准字符集兼容,还涵盖12个国家和地区的字符集,国际应用方面有优势。
  ·  PokerLinux:提供跨操作系统的基础架构,便于实现端到端方案的完整平台。
  ·  红旗Linux:中科院软件公司推出的嵌入式Linux,并有开放源码的Easy Embedded OS(EEOS)。
公开源码的BootLoader:U-BOOT、BLOB、VIVI、LILO、ARM-BOOT、RED-BOOT等。
根文件系统在嵌入式系统中一般设为只读,需要使用mkeramfs、genromfs等工具才能烧写映像文件。

2) 嵌入式Linux的软件组成:

嵌入式Linux包括引导加载程序、Linux内核、文件系统和应用程序4个层次,有些还包含图形用户界面。
⑴ 引导加载程序:
计算机从开机上电到操作系统启动需要一个引导过程,这个引导程序称Bootloader。通过这段程序,可以初始化设备、建立内存空间映射表,从而建立系统的软硬件环境,为加载操作系统内核做准备。系统一上电,一般都会从一个固定的地址开始运行,Bootloader的程序就要存储在相应的位置,这样CPU就会首先执行它。
对于嵌入式Linux,Bootloader是基于特定硬件平台来实现的,也依赖于系统板级的配置,不同的处理器架构、不同的开发板往往都有不同的Bootloader。但大部分Bootloader也有共性,有些就能支持多种体系结构。
Bootloader的启动过程通常是多阶段的,这样既能提供复杂的功能,又有很好的可移植性。大多数Bootloader都包含两种不同的操作模式,本地加载和远程下载。
⑵ Linux内核:
操作系统是管理计算机硬件和软件资源的程序,同时也是计算机系统的内核和基石,用来控制其他程序运行、管理系统资源,并为用户提供操作界面。Linux内核的主要模块分为存储管理、CPU和进程管理、文件系统、设备管理和驱动、网络通信以及系统的初始化、系统调用等。
Linux内核提供硬件抽象层、磁盘及文件系统控制、多任务等功能的的系统软件,并不是完整的操作系统。
⑶ 文件系统:
操作系统中负责管理和存储文件信息的软件称为文件管理系统,简称文件系统,是一种用于明确磁盘或分区上的文件的方法和数据结构,也就是在磁盘上组织文件的方法。文件系统对文件存储器空间进行组织和分配,负责文件存储,并对存入的文件进行保护和检索,包括建立文件,存入、读出、修改、转储文件,控制文件的存取,当用户不再使用时撤销文件等。
嵌入式系统使用flash作为存储介质,为此产生了专门设计的文件系统,包括Romfs、Cramfs、Ramfs、Tmpfs、JFFS2、YAFFS等,其中Ramfs是把所有文件放在RAM中运行,JFFS2适合于NOR FLASH存储,YAFFS2更适合大容量NAND FLASH存储。
⑷ 用户应用程序:
应用程序运行在操作系统之上,目的是完成特定的任务。

3) 嵌入式Linux开发流程:

嵌入式操作系统屏蔽了底层硬件的复杂性,使得开发者通过操作系统提供的API函数就能完成大部分工作,因此简化了开发过程,提高了系统的稳定性。
嵌入式系统因为资源受限,直接在嵌入式系统平台上编写软件比较困难,一般采用先在通用计算机上编写程序,通过交叉编译,生成目标平台上可以运行的二进制代码,最后下载到目标平台上运行的方法。
嵌入式交叉开发环境一般包括交叉编译、交叉调试器和系统仿真器。目前常用的交叉开发环境的典型代表是GNU工具链,目前已经可以支持x86、ARM、MIPS、PowerPC等处理器,一些商业开发环境还有Metrowerks CodeWarrior、ARM Software Development Tookits、SDS Cross compiler、WindRiver Tornado、Microsoft Embendded Visual C++等。

2. 搭建嵌入式Linux开发平台:

1) 安装VMware Workstation软件:

VMware Workstation是一款桌面虚拟计算机软件,提供用户可在单一的桌面上同时运行不同的操作系统,Window10系统最好使用VMware Workstation 12及以上版本。

2) 配置虚拟主机硬件:

安装完成后,打开虚拟软件,单击Create a new Virtual Machine图标开始创建虚拟计算机,选择Custom模式可以设置更详细参数。选择要安装的系统类型为Linux,发行版本Ubuntu 64-bit,单击Next按钮。

3) 安装Ubuntu 64-bit:

4) 安装VMware Tools:

VMware Tools能实现Windows主机与虚拟机之间的文件共享,同时支持自由拖曳功能,鼠标可以在虚拟机与主机之间无缝移动。菜单VM-Install VMware Tools安装。

5) 安装文本编辑器Vim:

通过sudo adp-get install vim命令完成。

6) 安装g++:

使用命令sudo apt-get install g++

7) 安装Android开发工具及依赖库:

  sudo apt-get install build-essential
  sudo apt-get install make
  sudo apt-get install gcc
  sudo apt-get install g++
  sudo apt-get install libc6-dev
  sudo apt-get install patch
  sudo apt-get install texinfo
  sudo apt-get install libncurses-dev
  sudo apt-get install git-core gnupg
  sudo apt-get install flex
  sudo apt-get install bison
  sudo apt-get install gperf
  sudo apt-get install libsdl-dev
  sudo apt-get install libesd0-dev
  sudo apt-get install libwxgtk2.6-dev
  sudo apt-get install built-essential
  sudo apt-get install zip
  sudo apt-get install curl
  sudo apt-get install ncurses-dev
  sudo apt-get install zliblg-dev
  sudo apt-get install valgrind
  sudo apt-get install libgtk2.0-0:i386
  sudo apt-get install libpangox-1.0-0:i386
  sudo apt-get install libpangoxft-1.0-0:i386
  sudo apt-get install libidn11:i386
  sudo apt-get install gstreamer0.10-pulseaudio:i386
  sudo apt-get install gstreamer0.10-pulseaudio-base:i386
  sudo apt-get install gstreamer0.10-pulseaudio-good:i386
  sudo apt-get install libxml2-utils

8) 安装JDK:

在home目录下新建JDK目录,并将jdk1.6.0_26.tar.bz2复制到JDK目录下,解压完成安装:
  #sudo tar -jxvf jdk1.2.0_26.tar.bz2 -C ./
配置环境变量:
  #sudo vim /ect/profile
最好加入:
  #JDK PATH
  export JAVA_HOME=/usr/local/java/jdk1.6.0_26
  export JRE_HOME=/usr/local/java/jdk1.6.0_26/ire
  export CLASSPATH=. :$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
  export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH
  export USE_CCACHE=1

执行:
  #source /etc/profile
查看环境变量有没有成功:
  #source /etc/profile

9) 安装TFTP服务:

简单文件传输协议TFTP(Trivial File Transfer Protocol)是TCP/IP族中基于UDP的一个用来在客户机和服务器之间进行简单文件传输的协议。
  $sudo apt-get install tftpd-hpa tftp-hpa
修改配置文件:
  $sodu vim /etc/default/tftpd-hpa
修改内容:
  TFTP_USERNAME="tftp"
  TFTP_DIRECTORY="/home/kitty/tftp_share"
  TFTP_ADDRESS="0.0.0.0:69"
  TFTP_OPTIONS="-l -c -s"

如果用户名为kitty,在主文件夹下建立共享文件夹tftp_share和更改文件权限,路径是/home/ kitty/tftp_share。示例:
  $mkdir /home/kitty/tftp_share
  $chmod 777 /home/kitty/tftp_share

重新启动服务:
  $sudo service tftpd_hpa restart
然后测试,在/home/kitty/tftp_share路径下新建test文本,并写入haha:
  $cd /home/kitty/tftp_share
  $echo "haha" > test

回到主文件夹加,并用TFTP下载test文件:
  $cd ~
  $tftp 127.0.0.1
  tftp> get test

10) 安装NFS服务:

网络文件系统NFS(Network File System)是Linux系统支持的文件系统中的一种,允许网络中的计算机之间通过TCP/IP网络共享资源。本地NFS客户端应用可以透明地读写位于远端NFS服务器上的文件,如同访问本地文件一样。
安装软件:
  $sudo apt-get install nfs-kernel-server nfs-common
修改配置文件:
  $sudo vim /etc/exports
在末行加入内容保存退出:
  /home/kitty/nfs_share *(rw,sync,no_root_squash)
创建共享文件夹,修改权限:
  $mkdir /home/kitty/nfs_share
  $chmod 777 /home/kitty/nfs_share

重启NFS服务:
  $sudo /etc/init.d/nfs-kernel-server restart
测试。把/home/kitty/nfs_share挂载到/mnt上,在/mnt中创建test文件,使用查看命令ls在/home/kitty/nfs_share上显示test文件,即表示安装成功。
  $sudo mount -o nolock,tcp 127.0.0.1:/home/kitty/nfs_share /mnt
  $touch /mnt/test
  $ls /home/kitty/nfs_share/

3. 嵌入式Linux交叉编译:

本地编译是指在当前编译平台下,编译出来的程序只能放到当前平台下运行。交叉编译是在一种平台上编译出能运行在体系结构不同的另一种平台上的程序。常见的嵌入式开发一般都是用交叉编译方式,在x86平台上开发而生成目标平台上运行的程序。

1) 交叉编译器:

在交叉编译时,用户需要在PC平台上安装对应的交叉编译器,然后用这个交叉编译器编译用户的源代码,最终生成可在目标平台上运行的代码。常用的交叉编译器有:
  arm-none-linux-gnueabi-gcc
  arm-cortex_a8-linux-gnueabi-gcc
  mips-malta-linux-gnu-gcc

其中标明了体系结构、提供编译器的厂商、内核kernel、系统等。其中,arm-none-eabi用于编译arm架构的裸机系统,包括ARM Linux的Bootloader、Kernel,不适用于编译Linux应用Application;arm-none-linux-gnueabi集成glibc库,主要用于基于ARM架构的Linux系统,可用于编译ARM架构的Bootloader、Kernel、Linux应用等;arm-eabi-xxx是谷歌推出的用于编译Android的编译器;arm-none-uclinuxeabi用于uCLinux,使用uclibc库。

2) 交叉编译器的安装:

比如在共享目录/mnt/hgfs/share/下复制arm-2009q3.tar.gz、arm-eabi-4.6.tar.gz到/usr/local /arm/下,执行命令:
  #cp /mnt/hgfs/share/arm-2009q3.tar.gz /usr/local/arm/
  #cp /mnt/hgfs/share/arm-eabi-4.6.tar.gz /usr/local/arm/

对压缩包解压:
  #tar zxvf arm-2009q3.tar.gz /usr/local/arm/
  #tar zxvf arm-eabi-4.6.tar.gz /usr/local/arm/

解压后的eabi目录改为4.6:
  #mv arm-eabi-4.6 4.6
设置环境变量,使之成为默认交叉编译器:
  #vi ~/.bashrc
在文件末尾添加一行指定路径:
  export PATH=/usr/local/arm/4.6/bin:$PATH
  export PATH=/usr/local/arm/arm-2009q3/bin:$PATH

刷新环境变量:
  #source ~/.bashrc
查看安装是否成功:
  #arm-linux-gcc -v
然后就可以编写程序进行交叉编译:
  #arm-linux-gcc hello.c -o hello -static
如果提示没有发现arm-linux-gcc,说明没有使用软连接,需要添加:
  #cd /usr/local/arm/4.6/bin
  #ln -s arm-none-linux-gnueabi-gcc arm-linux-gcc

在Android上运行程序必须用静态编译:
  #arm-linux-gcc hello.c -o hello -static

3) U-Boot编译:

在根目录root下新建U-Boot目录,在共享目录/mnt/hgfs/share/下复制gcc5260.uboot_linux. tar.bz2到/root/U-Boot中。
  #cp mnt/hgfs/share/gcc5260.uboot_linux.tar.bz2 /root/U-Boot
解压压缩包:
  #tar jxvf gcc5260.uboot_linux.tar.bz2
打开解压的压缩包文件:
  cd gec5260.uboot_linux
编译根文件配置文件:
  #make gec5260_config
查看Makefile文件的交叉编译工具链的路径:
  ifeq($(ARCH),arm)
  CROSS_COMPILE=/usr/local/arm/4.1.2/bin/arm-none-linux-gnueabi-

编译:
  #make clean
  #make gec5260_config
  #make -j4

查看当前路径生成的uboot.bin镜像文件。

4) U-Boot移植:

U-Boot即Universal Boot Loader,是指系统上电之后关闭WATCHDOG、改变系统时钟、初始化存储控制器等的一小段程序,需要针对不同的平台进行不同的移植。
一般需要修改延时时间、修改主机名、修改内存大小、修改dmc初始化文件、修改串口设置文件,还有环境变量设置等。

5) 编译内核:

根目录下新建zImage目录,在共享目录/mnt/hgfs/share/下复制linux-3.4.39-gec5260.tar.bz到zImage目录中:
  #cp mnt/hgfs/share/linux-3.4.39-gec5260.tar.bz /root/zImage
解压压缩包:
  #tar jxvf linux-3.4.39-gec5260.tar.bz
打开解压的压缩包文件:
  cd linux-3.4.39-gec5260
复制已经配置好的内核配置文件:
  #cp linux-2048x1536-config .config
查看Makefile文件的交叉编译工具链的路径:
  ARCH ?=arm
  CROSS_COMPILE ?=/usr/local/arm/4.1.2/bin/arm-none-linux-gnueabi-

编译:
  #make gec5260_defconfig
  #make -j4

最后查看当前路径arch/arm/boot生成的zImage镜像文件。
Linux提供了不同的工具来简化内核配置,但需要切换到root用户,并在内核源码目录中执行。可以使用命令行make config、基于ncurse图形界面make menuconfig、基于gtk+图形界面make gconfig,生成配置信息文件.config,存放在内核源码根目录下。也可以使用make defconfig对内核进行配置,该命令会根据体系结构创建一个配置。
下载Linux源码后,将其解压并存放在/usr/src目录下,开始编译:
  #make mrproper
  #make menuconfig
  #make
  #make install
  #make module_install

6) 内核移植:

U-Boot引导后,Linux启动。ARM架构一般都采用vmlinux启动过程。引导阶段通常使用汇编语言,实现了检查内核是否支持当前架构的处理器和支持当前的开发板。如果通过了检查,则为调用下一阶段的start_kernel函数做一些常规工作,如复制数据段、清除BSS段、调用start_kernel函数。
通用启动的过程主要以C语言编写,进行内核初始化的全部工作。
内核移植包括触摸驱动移植、LCD显示移植、更改内核启动logo等,还要修改内核机器码与U-Boot的机器码一致,修改开发板配置文件,修改Makefile文件,烧写内核镜像到开发平台等。

7) Android移植:

解压源码,cd到解压后的目录:
  #lunch
就会打印出选项。选择6,命令行输入make -j8,编译成功后生成镜像system.img和ramdisk. img,是升级必需的文件。
然后还要对WiFi移植。

4. 嵌入式Linux编程基础:

1) 内核启动过程:

Linux内核主要由进行调度、进程间通信、内存管理、虚拟文件系统、网络接口5部分组成。内核启动非常复杂,分6个步骤:
⑴ 实模式入口函数_start():
在header.s中,这里会进入main函数,它复制bootloader的各个参数,执行基本的硬件配置,解析命令行参数。
⑵ 保护模式入口函数startup_32():
在compressed/header_32.s中,这里会解压bzImage内核映像,加载vmlinux内核文件。
⑶ 内核入口函数startup_32():
在kernel/header_32.s中,就是进程0,它会进入体系结构无关的start_kernel()函数,即Linux内核启动函数,其中会做大量的内核初始化操作,解析内核启动的命令行参数,并启动一个内核线程来完成内核模块的初始化过程,然后进入空闲循环。
⑷ 内核模块初始化的入口函数kernel_init():
在init/main.c中,这里会启动内核模块,创建基于内存的rootfs。加载initramfs文件或cpio-initrd,并启动一个内核线程来运行其中的/init脚本,完成真正的根文件系统的挂载。
⑸ 根文件系统挂载脚本/init:
这里会挂载文件系统,运行/sbin/init,从而启动进程1。
⑹ init进程的系统初始化过程:
执行相关脚本,以完成系统初始化,例如设置键盘、字体、装载模块、设置网络等,最后运行登录程序,出现登录界面。

2) 模块的使用:

Linux本身是一个单内核系统,效率高,但可扩展性和可维护性相对较差。为了弥补这些缺陷,提供了模块机制。
模块是具有独立功能的程序,可以被单独编译,但运行时要被链接到内核作为内核的一部分在内核空间运行。
Linux中,可以手动或自动方式加载模块,手动使用insmod或modprobe命令实现,自动是通过内核线程kmod来实现,而kmod是通过调用modprobe来实现模块加载。
模块卸载也分手动与自动方式,手动使用rmmod命令,自动则通过kerneld或kmod自动卸载。

3) 开发常用的目录和文件:

① 程序目录:
Linux下的程序通常都保存在专门的目录中,如系统软件可在/usr/bin子目录找到,系统管理页为某个特定的主机系统或本地网络添加的程序可在/usr/local/bin子目录中找到。Linux的编译器gcc通常安装在/usr/bin或/usr/local/bin子目录中。
② 头文件目录:
C语言需要头文件来定义常数和系统及库函数的声明,这些头文件基本都保存在/usr/include及其下级子目录中,其他软件也可能有一些预先定义好的声明文件,保存位置可以被相应的编译器自动查到,比如X窗口的/usr/include/X11子目录等。
在调用C语言编译器时,也可以通过-I编译选项来引用保存在下级子目录或者非标准位置的头文件,如:
  [root@localhost linux]$ gcc -I /usr/openwin/include hello.c
该命令会使编译器在/usr/openwin/include子目录和标准目录两个位置查找hello.c程序中包含的头文件。
③ 库文件:
库文件是一些预先编译好的函数集合,通常是由一组互相关联的、用来完成某项常见工作的函数构成。Linux系统中可用的库大都存放在/usr/lib和/lib目录中,静态库后缀为.a,共享库后缀由.so和版本号组成。标准的系统库文件一般保存在/lib或/usr/lib子目录中。编译时需要告知编译器的链接程序查找库文件的位置。默认情况下,编译器只会查找C语言的标准库文件。
库文件必须遵守一定的命名规则,还必须在命令行上明确给出。在告知编译器查找某个库文件时,既可以给出完整的路径名,也可以使用-l选项:
  #arm-linux-gcc -o SQLitetest -I/home/nfs/sqlite3/include -L/home/nfs/sqlite3/lib -lsqlite3 SQLitetest.c
库文件在名称永远以lib开头,随后是说明库函数情况的部分,如libc.so用c来表示是一个C语言库,libm.so则用m表示是一个数学运算库。文件名的“.”后表示库文件类型:

.a 传统的静态型函数库
.so和.sa 共享型函数库
静态库:
按惯例都以.a为后缀,如C语言标准库为/usr/lib/libc.a,X11库为/usr/X11R6/lib/libX11.a等。
静态库是二进制目标代码文件,在编译时就已经连接到应用程序中。当有程序需要用到静态库中某个函数,要通过inluce语句引用对此函数的声明的头文件,而编译器和链接程序负责把程序代码和库函数结合在一起,称为一个独立的可执行文件。如果使用的不是标准的C语言静态库,而是一个扩展库,就必须用-l选项指定,如-lm。在gcc中使用-static选项时,使用静态链接库。
  ·  创建静态库:
要将fun1.c和fun2.c生成静态库,先要生成目标文件:
  #gcc -c fun1.c fun2.c
然后生成静态库:
  #ar -rcs libxxx.a fun1.o fun2.o
选项-r表示在库中插入模块,当模块名已存在时替换同名模块;-c表示无论库是否存在都将创建,不给警告;-s表示强制更新库的符号表,即使库的内容没有变化。
  ·  使用静态库:
  #gcc -o test_s test.c -L .-lxxx -static
选项-static表示强制使用静态库libxxx.a。如果将静态库放入了/usr/lib或/lib目录,可用去掉“-L .”。
共享库:
Linux支持共享库,其后缀一般为.so.N,其中N为版本号。C语言标准函数库为libc.so后接版本号,数学共享库libm.so.5,X-Windows库为libX11.so.6。GCC编译器会自动链接C标准函数库,但大部分系统函数库需要在命令行通过-l name显式指定所用的库名。
因为GCC编译器对所链接的共享库文件名要求以.so结尾,一般共享库文件名后有版本号,通常都会给真正的共享库文件建立符号链接文件,方式为:
  #ln -s libhello.so.1 libhello.so
在/usr/lib和/lib目录中可用找到绝大多数共享库,GCC编译器链接时会自动先搜索这两个目录。但也有一些库可能放置在特定目录中,如/etc/ld.so.conf配置文件中给出了这些目录的列表,链接程序会对配置文件中列出的这些目录搜索。默认情况下,Linux会先搜索共享库,找不到才会搜索静态库。
如果开发者自己编写了库文件,如果要让GCC自动找到该库,应放置在/usr/lib或/lib目录中,或者将该库文件的绝对路径加入/etc/ld.so.conf配置文件中,然后运行ldconfig命令更新。
共享库只在程序开始运行时才载入,在编译时只简单地指定需要的库函数。动态库是共享库的另一种变化形式,也是在程序运行时载入,但是在程序中的语句需要使用该函数时才载入,在程序运行期间就可以释放动态库占用的内存。由于共享库并没有在程序中包括库函数的内容,只是包含了对库函数的引用,因此程序的代码规模比较小。
  ·  创建共享库:
先要生成目标文件:
  #gcc -fpic -c fun1.c fun2.c
选项-fpic表示产生位置独立的代码。
然后生成共享库:
  #gcc -shared -o libxxx.so fun1.o fun2.o
选项-shared告知编译器生成共享库。
  ·  使用共享库:
  #gcc -o test_d test.c -lxxx
选项-lxxx表示自动加载名字为libxxx.so的库文件。

4) 内核的设备管理:

① devfs设备文件系统:
Linux 2.4引入,使得设备驱动程序能自主管理设备文件,挂载在/dev目录下。devfs可以通过程序在设备初始化时在/dev目录下创建设备文件,卸载设备时将它删除。不再需要为设备驱动程序分配主设备号和次设备号,但也有一些缺陷。
② sysfs文件系统:
Linux 2.6后引入,挂载于/sys目录下,把实际连接到系统上的设备和总线组织成一个分级的文件,提供接口给用户空间对驱动和设备进行配置,实现与用户空间的通信,用户可以通过echo、cat命令直接操作设备的属性文件,完成设备配置。
sys下面的目录和文件反映了整台机器的系统状况,真正的设备信息放在devices子目录下。
③ udev设备文件系统:
最初在Linux 2.6中使用,能够根据系统中设备的状态动态更新设备文件,包括设备的创建、删除等,是实现热插拔功能的主要工具。udev完全工作在用户态,利用设备加入或移除时内核发送的hotplug事件来工作,而关于设备的详细信息由内核输出到位于/sys的sysfs文件系统。udev结合sysfs才能完整实现设备的热插拔功能。
一般Linux系统使用创建静态设备的方法,因此/dev目录下创建了大量的设备节点,而不管对应的硬件设备是否实际存在,这通常是由makedev脚本完成,其中包含许多调用mknod程序的命令,为世界上可能存在的每个设备创建相应的主设备号和次设备号。使用udev方式,只有被内核检测到的设备才为其创建设备节点。

5) 同步机制:

① 中断屏蔽:
中断屏蔽是在单CPU范围内避免竞态的一种简单方法,在进入临界区之前屏蔽系统的中断。
  local_irq_disable()
当前内核应当尽快执行完临界区的代码,然后打开中断,不然可能造成数据丢失。
② 原子操作:
就是该操作绝不会在执行完毕前被任何其他任务或事件打断,即最小的执行单位。Linux内核提供了一系列函数来实现原子操作,分成整型和位操作。原子操作都需要硬件支持,很多函数都与CPU架构有关,并且是汇编语言实现的。
针对整数的原子操作只能对atomic_t类型的数据处理,位原子操作是对普通指针进行的操作。
③ 自旋锁spin lock:
自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被争用的自旋锁,那么该线程就会一直进行忙循环,等待锁重新可用。如果锁未被争用,请求锁的执行线程便会立刻得到它。
在中断处理程序中使用自旋锁,一定要在获取锁之前首先禁止本地中断,否则中断处理程序会打断正持有锁的内核代码,有可能会去争用这个已经被持有的自旋锁。
④ 读写自旋锁:
读写自旋锁为读和写分别提供了不同的锁,一个或多个任务可以并发地持有读锁,用于写的锁最多只能被一个写任务持有,而且不能有并发的读操作。
⑤ 顺序锁seqlock:
顺序锁是对读写锁的一种优化,读执行单元不会被写执行单元阻塞。读执行单元可以在写执行单元对被顺序锁保护的共享资源进行写操作时继续读,不必等待写执行单元完成写操作,写执行单元也不需要等待所有读执行单元完成读操作才进行写操作。
如果读执行单元在读期间,写执行单元已经发生了写操作,那么读执行单元必须重新读取数据,确保得到的数据是完整的。被保护的共享资源不能含有指针,因为写执行单元可能使指针失效。
⑥ RCU(Read-Copy Update):
RCU的原理就是读取-复制-更新,对于被RCU保护的共享数据结构,读执行单元不需要获得任何锁就可以访问,但写执行单元在访问时首先复制一个副本,然后对副本进行修改,最后使用一个回调机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据,这个时机就是所有引用该数据的CPU都退出对共享数据的操作。等待适当时机的这一时期称为宽限期。
RCU是一种改进的自旋锁,读执行单元没有任何同步开销,而写执行单元的同步开销取决于写执行单元间的同步机制。在写比较多的时候,对读执行单元的性能提高不能弥补写执行单元导致的损失。RCU已经在网络驱动层、网络核心层、IPC、dcache、内存设备层、软RAID层、系统调用审计和SELinux中使用。
⑦ 信号量:
Linux中的信号量是一种睡眠锁,如果一个任务试图获得已经占用的信号量时,信号量会将其推进一个等待队列,让其睡眠,这时处理器能重获自由,从而去执行其他代码。当持有信号量的进程将信号量释放后,处于等待队列中的那个任务被唤醒,并获得该信号量。
因为信号量会导致睡眠,所以不能用于中断上下文。信号量允许任意多个执行单元持有该信号量。
⑧ 完成量与读写信号量:
完成量是一个执行单元等待另一个执行单元执行完某事。
读写信号量允许多个读进程同时持有信号量,但最多只能有一个写进程。所有的读写信号量都是互斥信号量,只要没有写进程持有信号量,读进程就始终会持有该信号量,因此会造成写进程的饥饿状态。
⑨ 大内核锁BKL:
BKL是一个全局自旋锁,用来方便实现从Linux最初的SMP过渡到细粒度加锁机制。持有BKL的任务仍然可以睡眠,BKL是一种递归锁,可以用在进程上下文中,但在内核中不鼓励使用BKL,新代码中不再使用BKL。
⑩ 自旋锁与信号量的比较:
Linux中解决并发控制的最常用同步机制是自旋锁与信号量,二者在本质和实现机理上完全不同。
自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环查看该自旋锁是否已经释放;而信号量则引起调用者睡眠。
一般而言,自旋锁适合于保持时间非常短的情况,可以在任何上下文使用;信号量适合于保持时间较长的情况,只能在进程上下文使用。如果共享资源只在进程上下文访问,可以使用信号量;如果对共享资源的访问时间非常短,自旋锁也是好的选择;如果被保护的共享资源需要在中断上下文访问,就必须使用自旋锁。
持有信号量时可以去睡眠,而持有自旋锁时不允许睡眠。信号量保护的临界区可包含引起阻塞的代码,而自旋锁要避免用来保护包含这样代码的临界区,因为阻塞意味着要进行进程切换,如果进程切换出去,另一进程企图获取本自旋锁,死锁就会发生。信号量不会禁止内核抢占,所以持有信号量的代码可以被抢占,意味着信号量不会对调度的等待时间带来负面影响。

6) Linux调试方法:

① printk:
功能为打印字符串,可以在前面加上内核定义的宏。
② /proc文件系统:
这是一个伪文件系统,即虚拟文件系统,存储的是当前内核运行状态的一系列特殊文件。用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中的某些文件来改变内核的运行状态。
所有使用/proc的模块应当包含<linux/proc_fs.h>头文件,并定义正确的函数。/proc下的每个文件都绑定到一个内核函数上,当文件被读的时候就会即时产生文件内容。
③ 调试器:
gdb调试器和kdb内核调试器,Linux Trace Toolkit追踪工具,动态探针DProbes。

5. Linux设备驱动程序基础:

Linux内核驱动中,所有的设备都是以文件的概念出现,这样所有的读写操作等就会变得很方便。操作系统内核对外提供了相应的数据结构,可以方便地进行填充调用。
Linux下的设备驱动程序被组织为一组完成不同任务的函数的集合,通过这些函数使得Linux的设备操作犹如文件一样。在应用程序看来,设备只是一个设备文件,应用程序可以像操作普通文件一样对硬件设备进行操作。
Linux将设备分为两类,字符设备和块设备。字符设备是指那些只能一个字节一个字节读写数据的设备,不能随机读取设备内存中的某一数据。读取数据需要按照先后顺序。字符设备是面向数据流的,常见的有鼠标、键盘、串口、控制台、led等。块设备是指那些可以从设备的任意位置读取一定长度数据的设备,读取数据不必按照先后顺序,可以定位到设备的某一具体位置读取数据。常见的块设备有硬盘、U盘、SD卡等。
任何设备都有一个主设备号和一个次设备号,主设备号用来表示一个特定的驱动程序,次设备号用来表示使用该驱动程序的各设备。
设备文件的最初目的是允许进程同内核中的设备通信,并且通过它们和物理设备通信。其中每个设备驱动都对应着一定类型的硬件设备,并且被赋予一个主设备号,设备驱动的列表和它们的主设备号可以在/proc/devices中找到。每个驱动程序管理下的物理设备也被赋予一个次设备号,无论这些设备是否真的安装,在/dev目录中都有一个文件,称作设备文件,对应着每一个设备。
模块可以分成两部分,模块部分和设备及设备驱动部分,init_module函数调用module _register_chrdev在内核的设备表里注册设备驱动,同时返回该驱动所使用的主设备号;clearup_module函数撤销设备的注册。注册和注销是这两个函数的主要功能。

1) 字符设备驱动开发:

开发Linux字符设备驱动需要使用的关键数据结构为:
  struct file_operations{
     struct module *owner;
     loff_t(* llseek)(struct file *, loff_t, int);
     ssize_t(* read)(struct file *, char __user *, size_t, loff_t *);
     ssize_t(* aio_read)(struct kiocb *, char __user *, size_t, loff_t);
     ssize_t(* write)(struct file *, const char __user *, size_t, loff_t *);
     ssize_t(* aio_write)(struct kiocb *, const char __user *, size_t, loff_t);
     int(* readdir)(struct file *, void *, filldir_t);
     unsigned int(* poll)(struct file *, struct poll_table_struct *);
     int(* ioctl)(struct inode *, struct file *, unsigned int, unsigned long);
     int(* mmap)(struct file *, struct vm_area_struct *);
     int(* open)(struct inode *, struct file *);
     int(* flush)(struct file *);
     int(* release)(struct inode *, struct file *);
     int(* fsync)(struct file *, struct dentry *, int datasync);
     int(* aio_fsync)(struct kiocb *, int datasync);
     int(* fasync)(int, struct file *, int);
     int(* lock)(struct file *, int, struct file_lock *);
     ssize_t(* readv)(struct file *, const struct iovec *, unsigned long, loff_t *);
     ssize_t(* writev)(struct file *, const struct iovec *, unsigned long, loff_t *);
     ssize_t(* sendfile)(struct file *, loff_t *, size_t, read_actor_t, void __user *);
     ssize_t(* sendpage)(struct file *, struct page *, int, size_t, loff_t *, int);
     unsigned long(* get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
  };

结构体的说明:
  ·  struct module *owner,这是一个指向拥有这个结构的模块的指针,在操作还在使用时阻止模块被卸载。几乎所有时间,它初始化为THIS_MODULE,这是在<linux/module.h>中定义的宏。
  ·  llseek方法用作改变文件中的当前读/写位置,并且新位置作为返回值,负值为错误指示。
  ·  read用来从设备中获取数据,非负返回值代表成功读取的字节数。
  ·  aio_read初始化一个异步读。

2) 块设备驱动开发:

块设备只能以块为单位接收输入和返回输出,对于I/O请求有对应的缓冲区,因此可以选择以什么顺序进行响应,可以随机访问。但对磁盘设备,顺序访问可以提高性能。
在Linux中,驱动对块设备的输入输出操作,都会向块设备发出一个请求,在驱动中用request结构体描述。但对于一些磁盘设备,请求的速度很慢,这时内核就提供一种队列的机制,把这种I/O请求添加到队列中,在驱动中用request_queue结构体描述。在向块设备提交这些请求前,内核会先执行请求的合并和排序预操作,以提高访问效率,然后再由内核中的I/O调度子系统负责提交I/O请求,I/O调度程序将磁盘资源分配给系统中所有挂起的块I/O请求,其工作是管理块设备的请求队列,决定队列中的请求排列顺序及什么时候派发请求到设备。
在通用块层中,通常用一个bio结构体对应一个I/O请求,它代表了正在活动的以段Segment链表形式组织的块I/O操作,对于它所需要的所有段用bio_vec结构体表示。Linux提供了一个gendisk数据结构体,用它来表示一个独立的磁盘设备或分区,在gendisk中有一个类似字符设备中file_operations的硬件操作结构指针,就是block_device_operations结构体,主要定义块设备所支持的操作。
① 块设备关键数据结构:
gendisk数据结构:
在Linux内核中,使用gendisk结构体来表示一个独立的磁盘设备或分区,它存储了一个硬盘的信息,包括请求队列、分区链表和块设备操作函数集等,定义在linux/genhd.h中。
Linux内核提供了一组函数来操作gendisk:
  ·  分配gendisk:
gendisk结构体是动态分配的结构体,需要特别操作的内核操作来初始化,驱动不能自己分配这个结构体,而使用函数:
  struct gendisk * alloc_disk(int minors);
参数minors是这个磁盘使用的次设备号的数量,一般就是磁盘分区的数量,此后不能修改。
  ·  增加gendisk:
gendisk结构体被分配之后,系统还不能使用这个磁盘,需要调用如下函数来注册:
  void add_disk(struct gendisk * gd);
这个函数的调用要在驱动程序的初始化工作完成并能响应磁盘的请求之后。
  ·  释放gendisk:
当不再需要一个磁盘时,应当使用以下函数释放:
  void del_gendisk(struct gendisk * gd);
  ·  gendisk引用计数:
gendisk中包含1个kobject成员,因此是一个可被引用计数的结构体。通过get_disk()和put_disk()函数可用来操作引用计数,这个工作一般不需要驱动做。通常对del_gendisk()的调用会去掉gendisk的最终引用计数,但并不是一定的。因此,在del_gendisk()被调用后,这个结构体可能继续存在。
  ·  设置gendisk容量:
  void set_capacity(struct gendisk * disk, secror_t size);
块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,最常见的是512字节。扇区的大小是设备的物理属性,扇区是所有块设备的基本单元,块设备无法对比它还小的单元进行寻址和操作,许多块设备能够一次传输多个扇区。
block_device_operations数据结构:
在块设备驱动中,有类似字符设备驱动中file_operations的结构体block_device_operations,是对块设备操作的集合。
  ·  打开和释放:
  int (* open)(struct inode * inode, struct file * filp);
  int (* release)(struct inode * inode, struct file * filp);

  ·  IO控制:
  int (* ioctl)(struct inode * inode, struct file * filp, unsigned int cmd, unsigned long arg);
ioctl()系统调用的实现,块设备包含大量的标准请求,这些标准请求由Linux块设备层处理,因此大部分块设备驱动的ioctl()函数相当短。
  ·  介质改变:
  int (* media_changed)(struct gendisk * gd);
被内核调用来检查驱动器中的介质是否已经改变,如果改变返回值是非零值,否则返回0。这个函数仅适用于支持可移动介质的驱动器,非可移动设备的驱动不需要实现这个方法。通常需要在驱动中增加1个表示介质状态是否改变的标志变量。
  ·  使介质有效:
  int (* revalidate_disk)(struct gendisk * gd);
被调用来响应一个介质改变,它给驱动一个机会来进行必要的工作以使新介质准备好。
  ·  获得驱动器信息:
  int (* getgeo)(struct block_device *, struct hd_geometry *);
该函数根据驱动器的几何信息填充一个hd_geometry结构体,hd_geometry结构体包含磁头、扇区、柱面等信息。
  ·  模块指针:
  struct module * owner;
指向拥有这个结构体的模块的指针,通常被初始化为THIS_MODULE。
request数据结构:
在Linux块设备驱动中,使用request结构体来表征等待进行的I/O请求。
request_queue数据结构:
求队列跟踪等候的块I/O请求,存储用于描述这个设备能够支持的请求的类型信息。如果请求队列被正确配置,它不会交给该设备一个不能处理的请求。
  ·  初始化请求队列:
  request_queue_t * blk_init_queue(request_fn_proc * rfn, spinlock_t * lock);
该函数的第1个参数是请求处理函数的指针,第2个参数是控制访问队列权限的自旋锁。这个函数会发生内存分配的行为,故可能会失败,一定要检查返回值。一般在块设备驱动的模块加载函数中调用。
  ·  清除请求队列:
  void blk_cleanup_queue(request_queue_t * q);
这个函数完成将请求队列返回给系统的任务,一般在块设备驱动模块卸载函数中调用。
  ·  分配请求队列:
  request_queue_t * blk_alloc_queue(int gfp_mask);
对于Flash、RAM盘等完全随机访问的非机械设备,不需要复杂的I/O调度,应使用上述函数分配1个请求队列。
  ·  绑定请求队列和制造请求函数:
  void blk_queue_make_request(request_queue_t * q, make_request_fn * mfn);
bio数据结构:
通常一个bio对应一个I/O请求,I/O调度算法可将连续的bio合并成一个请求,因此一个请求可以包含多个bio。bio为通用层的主要数据结构,既描述了磁盘的位置,又描述了内存的位置,是上层vfs与下层驱动的连接纽带。
② 块设备驱动设计:
块设备驱动注册与注销函数:
  ·  注册函数:
  int register_blkdev(unsigned int major, const char * name);
参数major是块设备要使用的主设备号;name为设备名,会在/proc/devices中显示。如果major为0,内核会自动分配一个新的设备号,函数返回的就是这个主设备号。如果返回负值说明发生错误。
  ·  注销函数:
  int unregister_blkdev(unsigned int major, const char * name);
块设备驱动打开与关闭函数:
块设备驱动的open()和release()函数是非必须的,1个简单的块设备驱动可以不提供这两个函数。块设备驱动的open()函数和字符设备驱动中的类似,都以相关的inode和file结构体指针作为参数。当一个节点引用一个块设备时,inode->i_bdev->bd_disk包含一个指向关联gendisk结构体的指针。也可以将gendisk的private_data赋给file的private_data,也最好是指向描述该设备的设备结构体xxx_dev的指针。
在一个处理真实硬件设备的驱动中,open()和release()方法还应当设置驱动和硬件的状态,这些工作可能包括启停磁盘、加锁一个可移出设备和分配DMA缓冲等。
块设备驱动ioctl、read和write函数:
块设备可以包含一个ioctl()函数以提供对设备的I/O控制能力。实际上高层的块设备层代码处理了绝大多数ioctl(),具体的块设备驱动中通常不再需要实现很多ioctl()命令。
对于块设备,不用编写read()和write()函数,而是用VFS提供的通用函数block_write()和block_read(),即在数据结构file_operations中的read和write值是block_read()和block_write()。
块设备驱动的请求函数:
  ·  使用请求队列的情况:
块设备请求的原型为:
  void request(request_queue_t * queue);
这个函数不能由驱动自己调用,只有当内核认为是时候让驱动处理对设备的读写等操作时才调用这个函数。
  ·  不使用请求队列的情况:
对于存储卡、RAM盘等可以真正随机访问的设备,块层支持无队列的操作模式,驱动必须提供一个制造请求函数。
  typedef int(make_request_fn) (request_queue_t * q, struct bio * bio);

3) 网络设备驱动开发:

Linux中,网络系统是一个分层结构,一般为物理层/数据链路层、IP层、INET Socket层、BSD Socket层和应用层5个层次,前4部分包含在Linux内核中。INET Socket层实现对IP分组排序、控制网络系统效率等,IP层为TCP.IP网络协议栈的网络层实现。
网络设备应用层中的操作对象是socket文件描述符,通过文件系统定义的通用接口,使用系统调用从用户空间切换到内核空间,控制socket文件描述符对应的是对BSD Socket的操作,从而进入到BSD Socket层。在BSD Socket层中,操作对象是struct socket结构,每个结构对应一个网络连接。通过网络地址的不同来区分不同的操作方法,判断是否应进入到INET Socket层,这一层的数据存在struct msghdr结构体中,内核使用该结构体来接收数据。在INET Socket层,根据建立连接的类型,分成面向连接和无连接两种,区分TCP和UDP协议。这一层的操作对象是socket类型的数据,数据存放在sk_buff结构体中,sk_buff结构体是Linux网络代码中最重要的数据结构,它表示接收或发送数据包的包头信息。从INET Socket层到IP层,主要是路由过程,发送时根据发送的目标地址确定需要使用的网络设备接口和下一个需要传送到的机器地址,接收数据时需要在IP层判断该数据包是要发送给上一层协议还是需要做一个IP转发,将数据传递给下一台机器。从IP层到硬件层,也就是到网络接口设备驱动程序,是有关硬件的控制方法。
① 网络设备管理:
所有对网络硬件的访问都是通过接口进行的,网络接口看作是一个发送和接收数据包的实体。对于每个网络接口,都用一个device的数据结构表示。在内核启动时,通过网络设备驱动程序,登记存在的网络设备。设备用标准的支持网络的机制来传递收到的数据到相应的网络层,所有被发送和接收的包都用数据结构sk_buf表示。网络设备由其特殊之处,与字符设备及块设备都有很大不同。
网络接口不存在于Linux文件系统中,而是在核心中用一个device数据结构表示。网络设备在做数据包发送和接收时,直接通过接口访问,不需要进行文件操作。网络接口是在系统初始化时实时生成的。内核中存在网络接口管理表dev_base,是一条device结构链表的表头。在系统初始化后,系统检测到的网络设备将自动保存在这张链表中,其中每一个链表单元表示一个存在的物理物理设备。
当要发送数据时,网络子系统将根据系统路由表选择相应的网络接口进行数据传输;当接收到数据包时,通过驱动程序登记的中断服务程序进行数据的接收处理。
Linux网络设备驱动程序体系分为4层,网络协议接口层、网络设备接口层、设备驱动功能层、网络设备媒介层。网络设备驱动程序主要完成设备驱动功能层。
在Linux中,所有网络设备都抽象为一个接口,这个接口提供了对所有网络设备的操作集合。由数据结构struct net_device表示网络设备在内核中的运行情况,即网络设备接口,既包括纯软件网络设备接口,如环路,也包括硬件网络设备接口,如网卡。由以dev_base为头指针的设备链来集体管理所有网络设备,该设备链中的每一个元素代表一个网络设备接口。数据结构net_device中有很多供系统访问和协议层调用的设备方法,包括初始化、打开和关闭网络设备函数、处理数据包发送的hard_start_xmit函数,以及中断处理函数。
当网络子系统上层有数据包要发送时,通过调用网络设备驱动中实现的ndo_start_xmin函数,将要发送的数据包封装在套接字缓冲区skb参数中。在驱动程序的发送数据包函数额具体实现中,将首先在skb数据包所在主存中的数据块和网络设备内存之间建立一个DMA通道,然后启动该DMA,通道将数据包由主存传输到设备内存,之后由网络设备硬件通过网络接口或者天线将数据包发送出去。数据包发送成功后,会向处理器发送一个硬件中断,在中断处理程序中做一些善后处理工作。
数据包的接收是一个异步过程,绝大部分网络设备都支持数据接收中断,在驱动程序中是通过中断处理程序接收数据包。由于系统主存与网络设备之间已经建立好DMA通道,所以当有数据包到达网络设备时,数据包会被自动传输到系统主存,此时将产生一个中断信号,从而进入驱动程序的中断处理函数。在中断处理函数中,驱动首先会分配一个套接字缓冲区skb来容纳收到的数据包,然后将skb传递到网络子系统的上层代码中,是通过调用netif_rx(skb)函数实现的,上层代码负责释放该skb所占用的内存。
② NAPI机制:
NAPI是Linux采用的一种提高网络处理效率的技术,不采用中断方式读取数据,代之以首先采用中断唤醒数据接收的服务程序,然后以POLL的方法来轮询数据。目前NAPI技术已经在网卡驱动层和网络层得到广泛应用,已经完全用到netif_rx函数中,并提供process_backlog来处理轮询方法。NAPI技术可以大大提高短长度数据包接收的效率,减少中断触发的时间。
但NAPI也有一些缺陷。对于上层应用程序,系统不能在每个数据包接收到的时候都及时地去处理,而且随着传输速度增加,累计的数据包将会耗费大量的内存。还有就是对于大的数据包处理比较困难。因此,使用NAPI要使用DMA环形输入队列,或者有足够内存空间缓存驱动得到的包,在发送/接收数据包产生中断时,有能力关断NIC中断的事件处理,并且在关断后并不影响数据包接收到网络设备的环形缓冲区处理队列中。
NAPI对数据包到达事件采用轮询方法处理,在数据包到达时,NAPI就会强制执行dev->poll方法。
③ 关键数据结构:
Linux内核中采用net_struct的实例表示一个网络设备,其中包括了虚拟网络设备和实际网络设备,该数据结构比较复杂,主要任务分为两部分,对上层协议屏蔽底层设备的区别,提供统一的操作接口;对下层设备提供实际驱动方法。
网络设备驱动程序只需要填充net_device的具体成员,并注册net_device即可实现硬件操作函数与内核的挂接。
net_device结构体:
net_device_ops结构体:
head_ops结构体:
net_device_stats结构体:
④ 内核提供的网络设备驱动函数:
alloc_netdev:
  struct net_device * alloc_netdev(int sizeof_priv, const char * name, void (* setup)(struct net_device *));
其中,sizeof_priv是驱动的私有数据区大小;name是接口名;setup是一个初始化函数指针。
ether_setup:
这是一个通用的以太网接口的配置函数。
unregister_netdev:
模块卸载函数。
⑤ 网络设备驱动开发:
网络设备驱动设计主要是完成对net_device结构体的分配、初始化及注册。在初始化阶段需要调用module_init()、snull_init_module()、alloc_netdev()、register_netdev()、snull_cleanup()、snull_init()等函数。
操作函数的初始化主要通过两个结构体来完成将相关函数指针指向用户编写的驱动函数。打开关闭操作使用snull_open()和snull_release()函数,发送数据包使用snull_tx()函数,接收数据包使用snull_rx()函数。中断处理使用snull_regular_interrupt()函数,轮询处理使用snull_poll()函数。

4) 输入设备驱动:

input子系统是将Linux内核中的很多输入设备的软件进行分层,将属性相同且与硬件无关的部分抽象成事件核心层和事件处理层,由Linux系统统一实现,并以统一的接口提供给用户进程使用。输入子系统从下到上由设备驱动层、核心层、事件处理层组成,设备驱动层主要实现对硬件设备的读写访问、中断设置,并把硬件产生的事件转换成核心层定义的规范提交给事件处理层;核心层为设备驱动层提供规范和接口。设备驱动层关心如何驱动硬件并获得硬件数据,然后调用核心层提供的接口,核心层自动把数据提交给事件处理层。
事件处理层是用户编程的接口,并处理驱动层提交的数据。事件有三种属性,类型type、编码code、值value。input子系统支持的所有事件都定义在input.c中,包括所有支持类型、所属支持类型的编码等。设备驱动层把事件报告到事件核心层,核心层对事件进行分发,传到事件处理层,相应的事件处理层把事件放到event buffer中,等待用户进程来取。
① 输入设备驱动核心数据结构:
input_dev:是硬件驱动层,代表每个input设备
input_handler:是事件处理层,代表每个事件处理器
input_handle:是核心层,实现input_dev与input_handler的配对
一类handler可以和多个硬件设备相关联,一个硬件设备可以和多个handler相关联。比如,一个触摸屏设备可以作为一个event设备,作为一个鼠标鼠标,也可以作为一个触摸设备。
input子系统驱动的实现就是对以上3个结构体的操作。在编写设备驱动程序中,调用input_register_device()注册设备,其中初始化一些默认值,将device结构添加到Linux设备模型中,将input_dev添加到input_dev_list链表中,然后寻找合适的handler与input_handler配对,配对的核心函数是input_attach_handler。
input_attach_handler的主要功能是调用input_match_device进行配对,调用connect处理配对成功后续工作。每种事件处理器的connect实现都有差异。事件处理器函数为evdev_connect,分配一个evdev结构体并初始化相关成员,其中有input_handle结构,调用input_register_handle实现注册,把一个handle结构体通过d_node链表项分别链接到input_dev的h_list和input_handler的h_list上,从而实现3个结构体的连接。

5) 内核中断机制:

① 注册中断处理程序:
中断处理程序是管理硬件驱动程序的组成部分,每一部分都有相关的驱动程序,如果设备使用,那么相应的驱动程序就注册一个中断驱动程序。驱动程序可以通过下面的函数注册并激活一个中断处理程序,以便处理中断。
  int request_irq(unsigned int irq, irqreturn_t(* handler)(int, void *, struct pt_regs *), unsigned long irqflags, const char * devname, void * dev_id)
第1个参数irq表示要分配的中断号,这个值通常是预先设定的,对于大多数设备来说,这个值要么可以通过探测获取,要么可以通过编程动态设定;handler是一个指针,指向处理这个中断的中断处理程序,这个函数的原型是确定的,接受3个参数,其中一个类型为irqreturn_t的返回值;irqflags可以为0,也可能是一个或多个标志的掩码;devname是与中断相关设备的ASCII文本表示法,如keyboard,这个名字会被/proc/irq和/proc/interrupt文件使用,以便与用户通信;dev_id主要用于共享中断线request_irq(),成功执行返回0,如果返回非0就表示发生错误,最常见的错误是-EBUSY,表示给定的中断线已经在使用,释放中断线调用void free_irq(unsigned int irq, void *dev_id),当前用户没有指定SA_SHIRQ也会出现这种错误。
位掩码IRQF_DISABLED,表明给定的中断处理程序是一个快速中断处理程序,现在快速中断处理程序是在禁止所有中断的情况下运行,因此可以不受其他中断的干扰,除时钟中断外绝大多数中断都不使用此标志。位掩码IRQF_SAMPLE_RANDOM表明此设备产生的中断对内核entropy pool有贡献,这个pool负责提供从各种随机事件导出的真正随机数,如果指定此值,来自该设备的中断间隔时间就会作为entropy填充pool中。如果设备以预知的速率产生中断,或者可能受到外部攻击者的影响(如联网设备),就不要设置此标志,而其他很多硬件产生中断的速率不可预知,能成为较好的随机数来源。位掩码IRQF_SHARED表明可以在多个中断处理程序之间共享中断线,在同一个给定线上注册的每个处理程序必须指定这个标志,否则在每条线上只能有一个处理程序。
中断处理函数的返回值类型为irqreturn_t,可能返回2个推送至IRQ_NONE或IRQ_HANDLED。对于注册的中断处理程序,内核将注册信息保存到/proc/interrupts文件中。
② 编写中断处理程序:
  static irqreturn_t intr_handler)(int irq, void * dev_id, struct pt_regs * regs)
中断处理程序的操作取决于产生中断的设备及发送中断的原因,对于复杂的设备,可能需要在其中发送或接收数据,执行一些扩充的工作。
③ 软中断:
软中断数据结构:
  struct softirq_action{
     void(* action)(struct softirq_action *);   //待执行的函数
     void * data;   //传给函数的参数
  };

kernel/softirq.c中定义了一个包含有32个结构体的数组。
  static struct softirq_action soft_vec[32];
注册处理程序:
  open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
触发软中断:
  raise_softirq(NET_TX_SOFTIRQ);

④ 工作队列:
工作队列是另外一种将工作推后执行的形式,它将工作推后,交由一个内核线程去执行,工作队列允许重新调度甚至睡眠。如果推后执行的任务需要睡眠,就选择工作队列;如果推后执行的任务不需要睡眠,就选择软中断或tasklet。在需要获得大量内存、获得信号量、执行阻塞式I/O操作时,工作队列非常有用。
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务,它创建的这些内核线程称为工作者线程。默认的工作者线程叫做events/n,这里n是处理器编号,每个处理器对应一个线程。
工作者线程用workqueue_struct结构表示,工作常用work_struct结构表示。这些结构体连接成链表,在每个处理器上的每种类型的队列都对应这样一个链表。当一个工作者线程唤醒时,会执行它的链表上的所有工作。工作执行完毕,它就将相应的work_struct对象从链表上移去;当链表上不再有对象时,它就会继续休眠。
创建推后的工作:
  DELARE_WORK(name, void(* func) (void *), void *data);
静态地创建一个名为name处理函数func参数data的work_struct结构体。
工作队列处理函数:
  void work_handler(void * data);
这个函数会由一个工作者线程执行,因此函数会运行在进程上下文中,默认情况下允许相应中断。
对工作进行调度:
  schedule_work(&work);
work马上就会被调度,一旦其所在的处理上的工作者线程被唤醒,它就会被执行。
刷新操作:
  void flush_scheduled_work(void);
函数会一直等待,直到队列中所有对象都被执行后才返回。在等待所有待处理的工作的执行时候,该函数会进入休眠状态,所以只能在进程上下文中使用它。
创建新的工作队列:
  struct workqueue_struct * create_workqueue(const char * name);
这个函数会创建所有的工作者线程,系统中的每一个处理器都有一个,并且做好所有开始处理工作之前的准备工作。

6) 内核定时器的使用:

定时器由结构time_list表示:
  struct timer_list{
     struct list_head entry;   //包含定时器的链表
     unsigned long expires;   //以jiffies为单位的定时值
     spinlock_t lock;   //保护定时器的锁
     void (*function) (unsigned long);   //定时器处理函数
     unsigned long data;   //传给处理函数的长整数参数
     struct tvec_t_base_s *base;   //定时器内部值,用户不要使用
  }

内核提供了一组与定时器相关的接口来简化管理定时器的操作,所有这些接口都声明在timer.h中,大部分接口在文件timer.c中实现。
创建定时器需要先定义:
  struct timer_list my_timer;
接着需要通过一个辅助函数初始化定时器数据结构的内部值,初始化必须在使用其他定时器管理函数对定时器进行操作前完成:
  Init_timer(&my_timer);
然后就可以填充结构中需要的值了:
  my_timer.expires=jiffies+delay;   //定时器超时的节拍数
  my_timer.data=0;   //给定时器处理函数传入0值
  my_timer.function=my_function;   //定时器超时调用的函数

其中expires表示超时时间,是以节拍为单位的绝对计数值。如果当前的jiffies计数大于或等于expires,那么function指向的处理函数就会开始执行,该函数要使用长整数参数data。所以,定时器处理函数原型为:
  void my_timer_function(unsigned long data)
参数data可以用来利用同一个定时器处理函数注册多个定时器,只需通过该参数就能区分。如果不需要这个参数,可以简单传递0或其他值。
激活定时器:
  add_timer(&my_timer)
有时可能需要更改已经激活的定时器超时时间,内核通过函数mod_timer()来实现该功能:
  mod_timer(&my_timer,jiffies+new_delay);
mod_timer()也可以操作已经初始化但没有激活的定时器,如果定时器未被激活就会激活它。如果调用时定时器未被激活,该函数返回0,否则返回1。不论哪种情况,一旦从mod_timer()函数返回,定时器都将被激活,而且设置了新的定时值。
如果需要在定时器超时前停止定时器,可以使用del_timer()函数:
  del_timer(&my_timer);
被激活或未被激活的定时器都可以使用该函数。如果定时器还未被激活,该函数返回0,否则返回1。

7) 睡眠与唤醒:

当进程等待事件时,如输入数据、子进程的终止等,它需要进入睡眠状态以便其他进程可以使用计算资源。调用下面函数之一可以让进程进入睡眠状态:
  void interruptible_sleep_on(struct wait_queue **q)
  void sleep_on(struct wait_queue **q)
  int wait_event_interruptible(wait_queue_head_t q,int condition)

然后使用如下函数之一唤醒进程:
  void sleep_up(struct wait_queue **q)
  void sleep_up_interruptible(struct wait_queue **q)

6. 嵌入式Linux设备驱动的开发:

设备驱动程序是操作系统内核和硬件设备之间的接口,驱动程序是内核的一部分,实现驱动程序的注册和注销、设备的打开和释放、设备的读写操作、设备的中断和轮询处理等。
Linux外设可以分为字符设备、块设备和网络设备,字符设备I/O传输过程中以字符为单位进行传输;块设备将信息存储在固定大小的块中,每个块都有自己的地址,每个块都能独立于其他块读写;网络设备的I/O是成块的,但不是固定大小。

1) 实现一个嵌入式Linux设备驱动程序大致流程:

  ·  了解设备工作原理
  ·  定义主设备号
  ·  实现初始化函数,实现驱动的注册和注销
  ·  设计所要实现的操作,如open、close、read、write等函数
  ·  实现中断服务,通过request-irq向内核注册。并不是每个设备都需要中断
  ·  编译该驱动程序到内核中,或使用insmod命令加载
  ·  编写应用程序对驱动程序进行测试

2) Linux内核模块helloworld代码:

  #include   //所有驱动需要的头文件
  #include   //包含很多内核函数,如printk()
  static int __init exynos5260_hello_module_init(void) //初始化
  {
     printk("Hello, Exynos5260 module is installed !\n");
     return 0;
  }
  static void __exit exynos5260_hello_module_cleanup(void)   //卸载
  {
     printk("Good-bye, Exynos5260 module was removed !\n");
  }
  //驱动有关声明
  module_init(exynos5260_hello_module_init);   //加载驱动时执行
  module_exit(exynos5260_hello_module_cleanup);   //卸载驱动时执行
  MUDULE_LICENSE("GPL");   //符合开源驱动GPL协议

上述驱动分为4部分,头文件、初始化函数模块、退出函数模块、模块声明。

3) 编写Makefile文件:

  CONFIG_MYCHAR_DEV ?=m
  ifneq($(KERNELRELEASE),)
     hello -objects:hello.o
     obj-$(CONFIG_MYCHAR_DEV)+=hello.o
  else
  PWD:=$(shell pwd)   //指定编译好的驱动存放路径
  KERN_VER=$(shell uname -r)
  KERN_DIR=/root/linux-3.5   //指定内核源码路径
  modules:
     $(MAKE) -C $(KERN_DIR) M=$(PWD) modules
  endif
  clean:
     rm -rf *.o *.~core .depend *.cmd *.ko *.mod.c *.tmp_versions

4) 编译运行:

建立目录/root/helio_test/,将上述hello.c和Makefile复制到该目录下,输入make命令,即可生成目标文件hello.o。将hello.o下载到/tmp/目录下,在开发平台终端输入:
  #insmod hello.ko
  Hello, Exynos5260 module is installed !
  #lsmod |grep hello
  hello 605 0
  #rmmod hello
  Good-bye, Exynos5260 module was removed !

如果在向内核加入模块时发现insmod: error inserting 'hello.ko': -1 invalid module format,应检查KERNELDIR与当前系统的内核版本是否一致。

5) 驱动程序中编写ioctl函数:

⑴ 驱动结构体:
  static struct file_operations hello_fops={
    .owner=THIS_MODULE,
    .ioctl=hello_ioctl,
    .open=hello_open,   //外部测试调用open打开设备文件时触发
    .release=hello_close,   //外部调用close时触发
    .read=hello_read,   //外部read时触发
    .write=hello_write,   //外部write时触发
  };

其中,owner=THIS_MODULE表示驱动所有者为驱动模块本身。结构体中必须把供应用程序调用的函数登记下来,.ioctl=hello_ioctl登记了hello_ioctl函数,在应用程序中只需要使用inctl名即可。
⑵ ioctl函数:
此函数主要对设备进行控制,驱动函数中ioctl函数为:
  int (*ioctl) (struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg)
其中,inode和file是应用程序的文件描述符fd对应于驱动的inode和file两个指针,fd是打开设备文件的文件描述符;cmd是用户程序对设备的控制命令,具体命令的实现内容由驱动程序完成;一般没有可选参数arg,这时可用NULL代替。应用程序中调用ioctl函数:
  int ioctl(inf fd,int mod,NULL)
⑶ 设备号:
Linux中有字符设备和块设备,每个字符设备和块设备都必须有主、从设备号,主设备号相同的设备是同一类设备,使用同一个驱动程序。这些设备中,有些设备是对实际存在的物理硬件的抽象,而有些设备则是内核自身提供的功能(虚拟设备)。每个设备在/dev目录下都有一个对应的文件,可以通过cat/proc/devices命令查看当前已经加载的设备驱动程序的主设备号。内核能够识别的所有设备都记录在原码树下的documentation/devices.txt文件中。
使用ls命令查看:
  ls -1 /dev/rfd0
可以看到,原来显示文件大小的地方显示为用逗号分隔的数字,这是系统用来表示设备的两个重要序号,第一个为主设备号,用来表示设备使用的硬件驱动程序在系统中的序号,第二个为从设备号。
驱动程序的注册使用register_chrdev函数获得一个字符设备的主设备号:
  int register_chrdev(unsigned int major,const char *name,struct file_operations *fops)
其中,major是申请的主设备号,name是设备文件名,将要在文件/proc/devices中出现;fops是为了方便将设备文件的路径作为设备文件的名称,也是取得驱动程序结构体的入口地址。
驱动程序卸载可使用unregister_chrdev函数注销字符型设备所取得的设备号:
  int unregister_chrdev(unsigned int major,const char *name)
其中,major是驱动的主设备号,name是驱动设备名,需要与register_chrdev函数中的值保持一致。

6) LED驱动程序设计:

通过ioctl函数实现LED灯的开闭。首先在手册中查找相应寄存器,得到控制逻辑。程序中使用头文件:
  #include <linux/kernel.h>
  #include <linux/module.h>
  #include <linux/miscdevice.h>
  #include <linux/fs.h>
  #include <linux/types.h>
  #include <linux/moduleparam.h>
  #include <linux/slab.h>
  #include <linux/ioctl.h>
  #include <linux/cdev.h>
  #include <linux/delay.h>
  #include <linux/gpio.h>
  #include <linux/regs-gpio.h>
  #include <linux/gpio-cfg.h>
  #include <linux/gpio.h>
  //定义设备名:
  #define DEVICE_NAME "Led"
  struct led{
     int gpio;
     char *name;
  };
  //端口:
  static struct led led_gpios[]={
     {EXYNOS5260_GPD0(6),"led3"},
     {EXYNOS5260_GPD0(1),"led4"},
  };
  //定义常量
  #define LED_NUM 2 //ARRAY_SIZE(led_gpios)
  #define TEST_MAX_NR 2   //定义命令的最大序数
  #define TEST_MAGIC 'x'   //定义幻数
  //打开设备驱动程序
  static int led_open(struct inode *inode, struct file *filp)
  {
     printk(DEVICE_NAME " :open!\n");
     return 0;
  }
  //卸载设备驱动
  static void __exit gec5260_led_dev_exit(void)
  {
     int i;
     for(i=0;i<LED_NUM;i++){
       gpio_free(led_gpioi[i].gpio);
     }
     misc_deregister(&gec5260_led_dev);
  }
  //ioct控制led
  static long gec5260_leds_ioctl(struct file *filp, unsigned int cmd,unsigned int arg)
  {
     printk("led_num: %d\n",LED_NUM);
     if(_IOC_TYPE(cmd)!=TEST_MAGIC) return -EINVAL;
     if(_IOC_NR(cmd)>TEST_MAX_NR) return -EINVAL;
     gpio_set_value(led_gpio[_IOC_NR(cmd)].gpio,arg);
     printk(DEVICE_NAME " : %d %lu\n",_IOC_NR(cmd),arg);
     return 0;
  }
  static struct file_operations gec5260_led_dev_fops=
  {
     .owner=THIS_MODULE,
     .open=led_open,
     .unlocked_ioctl=gec5260_leds_ioctl,
  };
  static struct miscdevice gec5260_led_dev=
  {
     .minor=MISC_DYNAMIC_MINOR,
     .name=DEVICE_NAME,
     .fops=&gec5260_led_dev_fops,
  };
  static int __init gec5260_led_dev__init(void)
  {
     int ret,i;
     for(i=0;i<LED_NUM;i++){
       ret=gpio_request(led_gpios[i].gpio,led_gpios[i].name);
       if(ret){
         printk("%s: request GPIO %d for LED failed, ret=%d\n",
             led_gpios[i].name,led_gpios[i].gpio,ret);
         return ret;
       }
       s3c_gpio_cfgpin(led_gpios[i].gpio,S3C_GPIO_OUTPUT);
       gpio_set_value(led_gpios[i].gpio,0);
     }
     ret=misc_register(&gec5260_led_dev);
     if(ret==0)
       printk(DEVICE_NAME"\t initialized \n");
     else
       printk(DEVICE_NAME "\t initialized failed!\n");
     return ret;
  }
  //驱动有关声明
  module_init(gec5260_led_dev_init);   //加载驱动时执行
  module_exit(gec5260_led_dev_exit);   //卸载驱动时执行
  MUDULE_LICENSE("GPL");   //符合开源驱动GPL协议
  MUDULE_AUTHOR("Gec");   //作者

Makefile文件:
  obj-m+=led.o   //将驱动编程为module
  KERNEL_DIR := /home/gec/kernel.t5260.dev   //内核源码目录
  PWD := $(shell pwd)   //调用shell命令pwd,找到当前目录
  modules:
     $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
  clean:
     $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules clean

使用led驱动的应用程序:
  #define TEST_MAX_NR 2   //定义命令的最大序数
  #define TEST_MAGIC 'x'   //定义幻数
  #define LED1 _IO(TEST_MAGIC,0)
  #define LED2 _IO(TEST_MAGIC,1)
  #define LED3 _IO(TEST_MAGIC,2)
  #define LED4 _IO(TEST_MAGIC,3)
  int main(int argc,char **argv)
  {
     int fd,val;
     fd=open("/dev/Led",O_RDWR);
     if(fd<0)
     {
       perror("Can not open /dev/Led\n");
       return 0;
     }
     while(1)
     {
       val=0;
       printf("please select which light to turn on\n");
       printf("1 ->1:on\t 2 ->1:off\t 3 ->2:on\t 4 ->2:off\n");
       scanf("%d",&val);
       while(val==0)
       {
         scanf("%d",&val);
       }
       switch(val)
       {
         case 1:ioctl(fd,LED1,1);
           break;
         case 2:ioctl(fd,LED1,0);
           break;
         case 3:ioctl(fd,LED2,1);
           break;
         case 4:ioctl(fd,LED2,0);
           break;
         default:close(fd);
           return 0;
       }
     }
     return 0;
  }

应用程序的Makefile文件:
  Exec := led_test
  Obj :=led_test.c
  CC := arm-linux-gcc
  $(Exec) : $(Obj)
     $(CC) -o $@ $(Obj) $(LDLIBS$(LDLIBS-$(@)))
  clean:
     rm -vf $(Exec) *.elf *.o

测试:
建立/root/led_test/driver_code/目录,将led驱动源程序和Makefile文件复制到此目录下,执行make得到目标文件led_drv.ko,然后加载到开发板/tmp目录下。
建立/root/led_test/led_app/目录,将led应用源程序和Makefile文件复制到此目录下,执行make得到目标文件led_test,然后加载到开发板/tmp目录下。
执行:
  #./led_test 1 on

7) A/D模块驱动:

探测函数:
  static int __devinit exynos_adc_prob(struct platform_device *dev)
  {
     int ret;
     mutex_init(&adcdev.lock);
     adcdev.client=s3c_adc_register(dev,NULL,NULL,0);
     if(IS_ERR(adcdev.client)){
       printk("adc: cannot register adc\n");
       ret=PTR_ERR(adcdev.client);
       goto err_mem;
     }
     ret=misc_register(&misc);
     printk(DEVICE_NAME"\t initialized\n");
  err_mem:
     return ret;
  }

设备读取和控制函数:
  static ssize_t exynos_adc_read(struct file *filp,char *buffer,size_t count,loff_t *ppos)
  {
     char str[20];
     int value;
     size_t len;
     value=exynos_adc_read_ch();
     len=sprintf(str,"%d\n",value);
     if(count>=len){
       int r=copy_to_user(buffer,str,len);
       return r?r:len;
     }else{
       return -EINVAL;
     }
  }
  static long exynos_adc_ioctl(struct file *filp,int cmd,unsigned long arg)
  {
     #define ADC_SET_CHANNEL 0xc000fa01
     #define ADC_SET_ADCTSC 0xc000fa02
     sprintf("cmd is %d arg is %d\n",cmd,arg);
     switch(cmd){
       case ADC_SET_CHANNEL:
         exynos_adc_set_channel(arg);
         break;
       case ADC_SET_ADCTSC:
         //do nothing
         break;
       default:
         return -EINVAL;
     }
     return 0;
  }

模块入口及出口函数:
  static int __init exynos_adc_init(void)
  {
     return platform_driver_register(&exynos_adc_driver);
  }
  static int __exit exynos_adc_exit(void)
  {
     platform_driver_unregister(&exynos_adc_driver);
  }
  module_init(exynos_adc_init);
  module_exit(exynos_adc_exit);

8) MMC/SD驱动开发:

MMC/SD是两种比较常用的非易失存储卡,体积小、容量大、耗电低、传输速度快,广泛应用于消费电子产品中。MMC子系统的实现在kernel/driver/mmc目录下,不仅支持MMC/SD卡,也支持SDIO等存储卡。
① 关键数据结构:
  ·  struct mmc_host:用来描述卡控制器,位于kernel/include/linux/mmc/host.h
  ·  struct mmc_card:用来描述卡,位于kernel/include/linux/mmc/card.h
  ·  struct mmc_driver:用来描述MMC卡驱动,位于kernel/include/linux/mmc/card.h
  ·  struct mmc_host_ops:用来描述卡控制器操作集,用于从主机控制器向CORE层注册操作函数,从而将CORE层与具体的主机控制器隔离。位于kernel/include/linux/mmc/host.h
  ·  struct mmc_ios:用于描述控制器对卡的I/O状态,位于kernel/include/linux/mmc/host.h
  ·  struct mmc_request:用于描述读写MMC卡的请求,包括命令、数据及请求完成后的回调函数,位于kernel/include/linux/mmc/core.h
  ·  struct mmc_queue:是MMC卡的请求队列结构,封装了通用请求队列结构,位于kernel/include/linux/mmc/queue.h
  ·  struct mmc_data:描述了MMC卡读写的数据相关信息,例如请求、操作命令、数据、状态等,位于kernel/include/linux/mmc/core.h
  ·  struct mmc_command:描述了MMC卡操作相关命令及数据、状态等信息,位于kernel/include/linux/mmc/core.h
② MMC/SD卡设备驱动设计:
MMC子系统代码主要在drivera/mmc目录下,有3个子目录,其中:
  ·  Card:存放闪存卡的相关驱动
  ·  Host:针对不同主机端的SDHC、MMC控制器的驱动
  ·  Core:整个MMC的核心层,完成不同协议和规范的实现,提供接口函数
MMC/SD卡驱动分为卡识别阶段和数据传输阶段。卡识别阶段,通过命令使MMC/SD卡处于空闲idle、准备ready、识别ident、等待stby和不活动ina等状态;数据传输阶段,通过命令使MMC/SD卡处于发送data、传输tran、接收rcv、程序prg和断开连接dis状态。MMC/SD卡的工作过程分为两个阶段的10个状态。

9) USB驱动开发:

USB是通用串行总线Universal Serial Bus的缩写,为所有的USB外设提供了单一的、易于操作的连接类型,简化了外设设计,实现了单一的数据通用接口。一个USB系统包含主机HOST、设备DEVICE、集线器HUB三类硬件设备,支持热插拔hotplug和即插即用PHP,支持3种传输速率LS 1.5Mbps、FS 12Mbps、HS 480Mbps,一个USB口理论上可以连接127个设备。
USB采用属性拓扑结构,主机侧和设备侧的USB控制器分别称为主机控制器和USB设备控制器,每条总线上只有一个主机控制器,负责协调主机和设备间的通信,设备不能主动向主机发送任何消息。
① USB设备管理机制:
在Linux驱动中,处于最底层的是USB主机控制器硬件,在其上运行的是USB主机控制器驱动,再上面为USB核心层,再上层为USB设备驱动层,即插入主机上的U盘、鼠标、USB转串口等设备驱动。在主机侧,要实现USB驱动包括USB主机控制器驱动和USB设备驱动,前者控制插入其中的USB设备,后者控制USB设备如何与主机通信。
Linux内核USB核心负责USB驱动管理和协议处理,功能包括通过定义一些数据结构、宏和功能函数,向上为设备驱动提供编程接口,向下为USB主机控制器驱动提供编程接口;通过全局变量维护整个系统的USB设备信息;完成设备热插拔控制、总线数据传输控制等。
在USB设备的逻辑组织中,包含设备、配置、接口和端点4个层次。每个USB设备都提供了不同级别的配置信息,可以包含一个或多个配置,不同的配置使设备表现出不同的功能组合,配置由多个接口组成。
在USB协议中,接口由多个端点组成,代表一个基本的功能,是USB设备驱动程序控制的对象,一个功能复杂的USB设备可以具有多个接口。端点是USB通信的最基本形式,每个USB设备接口在主机看来就是一个端点的集合,主机只能通过端点与设备通信。在USB系统中,每个端点都有唯一的地址,由设备地址和端点号组成。每个端点都有一定的属性,其中包括传输方式、总线访问频率、带宽、端点号和数据包的最大容量等。一个USB端点只能在一个方向承载数据,可以看作单向管道。端点0通常为控制端点,用于设备初始化参数等,只要连接到USB上,并且上电,端点0就可以访问;端点1和2等一般用作数据端点,存放主机与设备间往来的数据。USB设备非常复杂,由许多逻辑单元组成。
② USB驱动关键数据结构:
设备描述符struct usb_device_descriptor:
这是关于设备的通用信息。
配置描述符struct usb_config_descriptor:
此配置中的接口数、支持的挂起和恢复能力及功率要求。
接口描述符struct usb_interface_descriptor:
接口类、子类和适用的协议,接口备用配置的数量和端口数目。
端点描述符usb_endpoint_descriptor:
端点地址、方向和类型,支持的最大包大小,如果是终端类型的端点还包括轮询频率。
usb_driver结构体:
描述一个USB设备驱动。
USB Request Block结构体struct urb:
描述与USB设备通信所用的基本载体和核心数据结构。
③ USB设备驱动函数:
客户端驱动管理:
USB内核通过一个双向链表usb_driver_list管理所用客户端驱动,功能为安装和卸载两部分,对应于usb_register和usb_deregister。USB内核是动态安装和卸载设备。
  int usb_register(struct usb_driver * new_driver);
  void usb_deregister(struct usb_driver * driver);

客户端驱动程序应在初始化函数中调用usb_register,先检查驱动是否初次安装,根据USBD保存的次版本号数组中该驱动对应项是否为空,如果不是,返回错误;如果是则加入到usb_driver_list中,并进行设备接口扫描usb_scan_devices,用来探测系统中哪些设备的接口可以被此程序驱动,将会调用驱动提供的probe函数。
驱动可以调用的其他接口管理函数:
  void usb_driver_claim_interface(struct usb_driver * driver, struct usb_interface * iface, void * priv);
  int usb_interface_claimed(struct usb_interface * iface);
  void usb_driver_release_interface(struct usb_driver * driver, struct usb_interface * iface);

USB设备配置和管理:
  ·  插入设备相关函数:
设备插入时,与之相连的集线器首先发现设备的插入信息,通过中断传输将信息传给集线器的驱动。通过信息分析,确认有新设备插入到总线上,集线器驱动调用usb_connect和usb_new_device来配置设备,并将其与对应的设备驱动建立联系,设定新设备信息,查找并分配设备地址dev→devnum。真正的USB命令进行配置由usb_new_device完成:
  void usb_connect(struct usb_device * dev);
  int usb_new_device(struct usb_device * dev);

参照协议,完成新设备配置,包括usb_set_address分配地址、usb_get_descriptor获得设备描述符、usb_get_configuration获得设备所有配置描述符、usb_set_configuration激活默认配置、usbdevfs_add_device加入一个/proc/bus/usb入口,通过usb_find_drivers为默认配置0的每一个接口查找相应的驱动程序来进行驱动。
总线上的第1个设备根集线器和主机控制器是一体的,在启动时就认为是插上的,默认地址为0。Linux支持多USB总线,即多个主机控制器和根集线器,它们各自的设备地址是不相关的。
  ·  拔下设备相关函数:
设备拔下时,与之对应的集线器首先检测到设备的拔下信号,通过中断传输将信息传送给集线器的驱动。集线器的驱动先验证设备是否被拔下,如果是则调用usb_disconnect进行处理。
  void usb_disconnect(struct usb_device ** pdev);
断开设备后,找到设备当前活动配置的每个端口的驱动程序,调用提供的disconnect接口函数,中断与各个接口的数据传输操作,释放为每个接口分配的资源。如果设备为集线器,则递归调用usb_disconnect处理它的子设备。释放设备地址,并通过usbdevfs_remove_device释放给设备创建的inode,usb_free_dev释放USBD给设备分配的资源。
  ·  设备复位相关函数:
  int usb_reset_device(struct usb_device * dev);
USBD提供了usb_reset_device进行设备的复位操作。首先复位设备连接的集线器端口,然后与usb_new_device函数相似的步骤重新完成对设备的配置。
④ 主机控制器的管理:
每个主机控制器拥有一个USB系统,称为USB总线。USBD支持多个主机控制器,即多个USB总线,每增加一个主机控制器会分配一个usb_bu结构,USBD动态安装和卸载主机驱动。
驱动安装时,初始化函数一方面完成主机控制器硬件的配置和初始化工作,另一方面调用usb_alloc_bus和usb_register_bus将自己注册到USBD中去,供USB子系统访问。
  struct usb_bus * usb_alloc_bus(struct usb_operations * op);
创建主机控制器对应的总线结构usb_bu,保存主机控制器给USBD提供的函数接口,并进行初始化。每个主机控制器都为USBD提供了一套函数接口usb_operations,进行USB通信。
将USB总线结构usb_bus注册到USBD中,即将其加入到USB内核的总线双向链表usb_bus_list中,并创建一个/proc/bus/usb入口。主机驱动卸载时,调用usb_deregister_bus和usb_free_bus释放资源。
为主机驱动提供接口的函数:
  struct usb_device * usb_alloc_dev(struct usb_device * parent, struct usb_bus * bus);
  void usb_free_dev(struct usb_device * dev);
  void usb_inc_dev_use(struct usb_device * dev);

⑤ 协议控制命令集和数据传输管理:
USBD为设备的客户端驱动提供了一套控制命令的接口函数,实现对设备的配置、控制和通信,这些接口函数通过usb_control_msg进行发送,客户端也可以通过调用usb_control_msg完成自己的设备命令。usb_control_msg是同步通信函数,不采用异步回调方式。
  int usb_clear_halt(struct usb_device * dev, int pipe);
上述函数只提供针对停止工作的端点的清除操作,不提供清除设备的远程唤醒操作。
数据传输都是适用USB内核提供的URB,有些变量是针对特定传输类型的。USBD提供的用于处理URB的接口函数有:
  ·  urb_t * usb_alloc_urb(int iso_packets):用于给客户驱动分配URB,参数iso_packets为该URB中需要传输的实时数据包的个数,其他传输为0
  ·  void usb_free_urb(urb_t * urb):释放分配的URB
  ·  int usb_submit_urb(urb_t * urb):将一个或多个URB异步发送给USB内核处理
  ·  int usb_unlink_urb(urb_t * urb):表示在URB传输完成之前取消数据处理,一般是设备在工作过程中被拔下,或软件主动取消对某个数据传输的处理,或URB传输超时时调用
当主机控制器驱动将URB中的数据传输处理完成后,将调用urb->complete回调函数来通知客户驱动。当URB处理超时,客户端驱动调用usb_unlink_urb通知HCD取消对此URB数据的传输。
⑥ 写USB驱动程序的基本步骤:
  ·  定义驱动程序要支持的设备
  ·  注册USB驱动程序
  ·  探测和断开
  ·  提交和控制USB请求块urb

10) I2C总线驱动开发:

① 总线驱动过程:
总线是处理器与设备之间的通道,在设备模型中,所有设备都通过总线相连。总线驱动数据结构定义为struct bus_type,主要定义是是name和match成员,uevent为热插拔前对环境变量的设置。定义一个总线设备,对CPU核心来说,总线也只是个外设而已,所以需要定义总线设备struct device,主要定义的成员是BUS_ID和release,定义总线属性BUS_ATTR(version, S_IRUGO, show_bus_version, NULL)。
在总线的几个基本对象定义后,在module_init中注册总线:先要使用bus_register(struct bus_type)注册,建立属性文件create_bus_file(不是必须),然后就可以在sys/bus/../看到总线属性,在注册总线设备。
在总线驱动中定义设备注册和设备驱动注册的数据结构,包括struct device成员的自定义_device设备结构、struct device_driver的自定义_driver驱动结构,还要在驱动结构中定义probe、remove等方法。
然后要实现两个注册过程:
  int register_xx_device(struct xx_device *)
  int register_xx_driver(struct xx_driver *)

前一个过程,设置device里的bus类型,此类型把设备注册到总线上;设置device parent指向总线注册时注册的总线设备;定义一个dev.release;复制一个名字到dev.bus_id中,这个名字要与自定义的driver结构的name成员一致才能注册成功;注册设备自定义中的device成员device_register;卸载过程使用device_unregister。
后一个过程,设置设备驱动结构中的device_driver成员的bus_type;在设备驱动程序中定义了probe等方法,则调用总线的probe把设备驱动中的probe等方法注册到内核中;还有其他一些相关的device_driver定义;注册device_driver;可选的属性文件建立。
有了以上的总线驱动,可以直接调用定义的设备注册函数和设备驱动注册函数来注册设备。要注意结构中的bus_id和设备驱动中的device_driver.name必须一致。注册成功后,一般会自定义probe函数,其带的参数是自定义的设备的数据结构,在驱动注册到内核过程中,系统找到与之匹配的设备后,会把这个设备的指针传给probe作为参数使设备驱动能得到这个参数,然后可以把其看成 一个句柄来使用,通过这个句柄可以调用总线上的方法。
② I2C总线:
I2C总线是由SDA和时钟SCL构成的串行总线,可发送和接收数据,最高速率100kbps,用于连接MCU及其外围设备,可以对各个组件进行查询,以管理系统的配置或掌握组件的功能状态,可随时监控内存、硬盘、网络及系统温度等多个参数,方便管理。I2C总线的主要优点是简单性和有效性,由于接口直接在组件上,因此I2C总线占用的空间非常小,减小了电路板的空间和芯片引脚的数量,降低了互联成本。总线长度可达25英尺,并且能以10kbps的最大传输率支持40个组件。
CPU发出的控制信号分为地址码和控制量两部分,地址码用来选址,控制量决定调整的类别及需要调整的量。I2C总线在传送数据过程中有开始信号、结束信号、应答信号3种类型信号。开始信号为SCL为高电平时,SDA由高电平向低电平跳变,开始传输数据;结束信号为SCL为低电平时,SDA由低电平向高电平跳变,结束传送数据;应答信号为接收数据的IC在接收到8b数据后,向发送数据的IC发出特定的低电平脉冲,表示已收到数据。CPU向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,判断受控单元出现故障。
③ Linux的I2C体系结构:
I2C核心提供了I2C总线驱动和设备驱动的注册及注销方法,总线驱动主要包含了I2C适配器数据结构i2c_adapter、I2C适配器的数据结构i2c_algorithm和控制I2C适配器产生通信信号的函数。经由I2C总线驱动的代码可以控制I2C适配器以主控方式产生开始位、停止位、读写周期,以及从设备方式被读写、产生ACK等。
在Linux内核中,所有的I2C设备都在sysfs文件系统中显示,存在于/sys/bus/i2c/目录中,以适配器地址和芯片地址的形式列出。在Linux内核源代码中的drivers目录下包含一个i2c目录,其下包含如下文件或文件夹:
  ·  i2c-core.c:实现了I2C核心的功能及/proc/bus/i2c *接口
  ·  i2c-dev.c:实现了I2C适配器设备文件的功能,每一个I2C适配器都被分配一个设备,通过适配器访问设备时的主设备号都为89,次设备号为0~255。应用程序通过“i2c-%d”文件名并使用文件操作接口函数open()、write()、read()、ioctl()、close()等来访问这个设备。应用层可以借用这些接口函数访问挂接在适配器上的I2C设备的存储空间或寄存器,并控制I2C设备的工作方式
  ·  chips文件夹:包含了一些特定的I2C设备驱动,例如一些时钟芯片、EEPROM驱动等
  ·  busses文件夹:包含了一些I2C总线的驱动
  ·  algos文件夹:实现了一些I2C总线适配器的实现
④ I2C驱动关键数据结构:
内核中的i2c.h头文件定义了4个数据结构:
i2c_adapter结构体:
i2c_algorithm结构体:
i2c_driver结构体:
i2c_client结构体:
⑤ Linux的I2C核心:
I2C核心drivers/i2c/i2c-core.c提供了一组不依赖于硬件平台的接口函数,这个文件一般不需要修改,主要函数包括:
增加/删除i2c_adapter:
  int i2c_add_adapter(struct i2c_adapter * adap);
  int i2c_del_adapter(struct i2c_adapter * adap);

增加/删除i2c_driver:
  int i2c_register_driver(struct module + owner, struct i2c_driver * driver);
  int i2c_del_driver(struct i2c_driver * driver);
  inline int i2c_add_driver(struct i2c_driver * driver);

i2c_client依附/脱离:
  int i2c_attach_client(struct i2c_client * client);
  int i2c_detach_client(struct i2c_client * client);

当一个具体的client被侦测到并被关联的时候,设备和sysfs文件将被注册;相反在client被取消关联的时候,sysfs文件和设备也被注销。
i2c传输、发送和接收:
  int i2c_transfert(struct i2c_adapter * adap, struct i2c_msg * msgs, int num);
  int i2c_master_send(struct i2c_client * client, const char * buf, int count);
  int i2c_master_recv(struct i2c_client * client, char * buf, int count);

i2c_transfert()函数用于进行I2C适配器和I2C设备之间的一组信息交互,i2c_master_send()函数和i2c_master_recv()函数内部会调用i2c_transfert()函数分别完成一条写消息和一条读消息。
I2C控制命令分派:
  int i2c_control(struct i2c_client * client, unsigned int cmd, unsigned long arg);
  int i2c_clients_command(struct i2c_adapter * adap, unsigned int cmd, void * arg);

上面函数有助于将发给I2C适配器设备文件ioctl的命令,分派给对应适配器的algo_control()函数或i2c_driver的command()函数。
⑥ Linux I2C总线驱动:
I2C总线驱动模块的加载函数要初始化I2C适配器所使用的硬件资源、申请I/O地址、中断号等,通过i2c_add_adapter()添加i2c_adapter的数据结构;I2C总线驱动模块的卸载函数要完成的工作与加载函数相反,释放I2C适配器所使用的硬件资源、释放I/O地址、中断号等,通过i2c_del_afapter()删除i2c_adapter的数据结构。
⑦ I2C总线通信方法:
为特定的I2C适配器实现其通信方法,主要实现i2c_algorithm的master_xfer()和functionality()函数。functionality()用于返回algorithm所支持的通信协议,如I2C_FUNC_I2C、I2C_FUNC_10BIT_ADDR、I2C_FUNC_SMBUS_READ_BYTE、I2C_FUNC_SMBUS_WRITE_BYTE等。
master_xfer()函数在I2C适配器上完成传递给它的i2c_msg数组中的每个I2C消息。master_xfer()函数模板中的i2c_adapter_xxx_start()、i2c_adapter_xxx_setaddr()、i2c_adapter_xxx_wait_ack()、i2c_adapter_xxx_readbytes()、i2c_adapter_xxx_writebytes()、i2c_adapter_xxx_stop()用于完成适配器的底层硬件操作,与I2C适配器和CPU的具体硬件直接相关,需要根据芯片的数据手册来实现。

11) PCI总线驱动开发:

PCI(Peripheral Component Interconnect)含义是外围设备互联,是目前一种通用的计算机总线标准。PCI提供了一组完整的总线接口规范,刻画了外围设备在连接时的电气特性和行为规范,详细定义了计算机系统中的不同部件之间的交互过程。PCI总线的时钟频率一般在25~33MHz范围,设置达到66MHz或133MHz,在64位系统中最高能达到266MHz。
在目前典型的计算机系统中,CPU和RAM需要通过PCI桥连接到主PCI总线0,而PCI显卡可以直接连接到主PCI总线上;PCI-PCI桥负责将PCI总线0和PCI总线1连接在一起,以太网卡、SCSI卡等则连接到PCI总线1上。
① PCI设备:
所有符合PCI总线标准的设备称为PCI设备,PCI总线架构中可以包含多个PCI设备。一个PCI接口卡上可能包含多个功能模块,每个模块都被当作一个独立的逻辑设备,因此每个PCI功能(逻辑设备)都唯一地对应一个pci_dev设备描述符,这是一个结构体。
② PCI设备驱动:
PCI设备上有3种地址空间,PCI的I/O空间、存储空间、配置空间。CPU可以访问PCI设备上的所有地址空间,其中I/O空间和存储空间提供给设备驱动程序使用,配置空间由Linux内核中的PCI初始化代码使用。
内核启动时负责对所有PCI设备进行初始化,配置好所有的PCI设备,包括中断号及I/O基址,并在文件/proc/pci中列出所有找到的PCI设备,以及这些设备的参数和属性。其中使用的结构体:
pci_driver:
在文件includes/linux/pci.h中定义,最主要的是用于识别设备的id_table结构,以及用于检测设备的函数probe()和卸载设备的函数remove()。
pci_dev:
也在文件includes/linux/pci.h中定义,详细描述了一个PCI设备几乎所有的硬件信息,包括厂商ID、设备ID及其各种资源。
实现PCI设备驱动时,通常要实现初始化设备、设备打开、数据读写和控制、中断处理、设备释放、设备卸载等。

12) Flash驱动开发:

现在市场上主要有NOR和NAND两种Flash存储器,应用程序可在NOR Flash芯片内执行,传输效率高,但写入和擦除速度很低;NAND Flash能提供极高的单元密度,写入和擦除也较快,但管理需要特殊接口。
Linux提供了内存技术设备MTD(Memory Technology Device)建立Flash的抽象统一接口,将MTD文件系统与低层的Flash存储器进行了隔离。MTD在Linux中的块设备主设备号为31,字符设备主设备号为90,设备定义在mtdblock.c及mtdchar.c中,通过注册一系列文件操作函数可实现对MTD设备的读写和控制。
① 关键数据结构:
MTD设备层字符设备:mtdchar.c中的mtd_notifier、mtd_fops
MTD设备层块设备:mtdblock.c中的mtd_notifier、mtd_fops、mtdblks
原始设备层:mtdcore.c和mtdpart.c中的mtd_notifiers、mtd_table、mtd_info、mtd_part
相关函数及数据结构:
register_mtd_user()、get_mtd_device()、unregister_mtd_user()、put_mtd_device()、erase_info
add_mtd_partitions()、del_mtd_partitions()、add_mtd_device()、del_mtd_device()、mtd_partition
mtd_info结构体:
用于描述MTD原始设备的数据结构是mtd_info,定义在include/linux/mtd/mtd.h文件中,其中定义了大量关于MTD数据和操作函数,每个分区也认为是一个mtd_info。
mtd_table结构体:
用来存放mtd_info的指针,是多个结构体的集合。mtdcore.c中定义了MTD设备数组:
  static struct mtd_info * mtd_table[MAX_MTD_DEVICES];
mtd_part结构体:
mtd_part用于描述分区,定义在drivers/mtd/mtdpart.c文件中:
  struct mtd_part{
     struct mtd_info mtd;   //分区的信息
     struct std_info * master;   //该分区的主分区
     uint64_t offset;   //该分区的偏移地址
     struct list_head list;
  }

mtd_partition结构体:
在MTD原始设备层调用add_mtd_partitions()时传递分区信息用,定义在include/linux/mtd/partitions.h文件中:
  struct mtd_partition{
     char name;
     uint64_t size;   //分区大小
     uint64_t offset;   //主MTD空间内的偏移地址
     uint32_t mask_flags;   //掩码标志
     struct nand_ecclayout * ecclayout;
  };

map_info结构体:
主要是针对NOR Flash,定义在include/linux/mtd/map.h文件中,指定了基址、位宽、大小等信息及读写函数,对于驱动NOR Flash很关键。
② 驱动相关函数:
add_mtd_device():
用来注册MTD设备,定义在drivers/mtd/mtdcore.c文件中,是add_mtd_partitions()函数的底层调用。如果Flash不分区,可以在驱动代码中直接调用。
del_mtd_device():
用来注销MTD设备,定义在drivers/mtd/mtdcore.c文件中,是del_mtd_partitions()函数的底层调用。如果Flash不分区,可以在驱动代码中直接调用。
add_mtd_partitions():
对每一个新建分区建立新的mtd_part结构体,将其加入mtd_partitions中,并调用add_mtd_device()将此分区作为MTD设备加入mtd_table。成功则返回0,如果内存不足返回-ENOMEM。
在drivers/mtd/mtdpart.c文件中定义。
do_map_probe():
用来探测Flash是否得到mtd_info,定义在includes/mtd/chips/chipreg.c中,在includes/linux/mtd/map.h中也有声明。函数原型:
  struct mtd_info * do_map_probe(const char * name, struct map_info * map);
其中,第1个参数为探测的接口类型,常见调用方法为:
  do_map_probe("cfi_probe", &xxx_map_info);
  do_map_probe("jedec_probe", &xxx_map_info);
  do_map_probe("map_rom", &xxx_map_info);

do_map_probe()根据传入的参数name,通过get_mtd_chip_driver()得到具体的MTD驱动,调用与接口对应的probe()函数探测设备。
③ NOR Flash驱动:
主要工作包括:
  ·  定义map_info实例,初始化其中的成员,根据目标板情况为name、size、bankwidth和phts赋值
  ·  如果Flash要分区,则定义mtd_partition数组,将Flash分区信息记录其中
  ·  以map_info和探测接口类型为参数调用do_map_probe(),得到map_info
  ·  在模块初始化时以map_info为参数调用add_mtd_device()或以map_info、mtd_partition数组及分区数为参数调用add_mtd_partitions()注册设备分区。在此之前可以调用parse_mtd_partitions()查看Flash上是否已有分区信息,并将查看出的分区信息通过add_mtd_partitions()注册
  ·  卸载模块时调用4步以删除设备或分区
④ NAND Flash驱动:
主要工作包括:
  ·  如果Flash分区,则定义mtd_partition数组,将实际电路板中Flash分区信息记录其中
  ·  在模块加载时,为每一个chip分配mtd_info和nand_chip的内存,根据目标板nand控制器的情况初始化nand_chip中的hwcontrol()、dev_ready()等成员函数填充mtd_info,并将其成员priv指向nand_chip
  ·  以mtd_info为参数,调用nand_scan()函数探测NAND Flash的存在,从中读取参数,填充到相应的nand_chip成员
  ·  如果要分区,则以mtd_info和mtd_partition为参数调用add_mtd_partitions()添加分区信息

7. 嵌入式Web服务器:

嵌入式设备上运行一个Web服务器能够生成动态页面,在用户端只需要浏览器就可以对嵌入式设备进行管理和监控,方便实用。因为嵌入式设备资源有限,并且也不需要能同时处理很多用户的请求,大多为单进程服务,只有完成一个用户请求后才能响应另一个用户的请求。
① Boa:
Bao诞生于1991年,作者是Paul Philips,官网www.bao.org。Bao是开源的,特别适合于嵌入式设备,可在所有兼容POSIX的操作系统上运行。Bao是一个单任务HTTP Server,对所有的活动的HTTP连接在内部进行处理,而且只为每个CGI连接开启新的线程,因此在同等硬件条件下显示出更快的速度。Bao内存需求非常少,导致能耗很小,特别适合于嵌入式市场。Bao功能强大,支持HTTP认证、CGI等,但不具有访问控制。
② thttpd:
这是ACME公司设计的一款简单、小巧、易移植、快速和安全的开源Web服务器,在Unix平台上运行的二进制代码仅仅400kB左右,官网www.acme.com/software/httpd/。thttpd在默认情况下仅运行于普通用户模式下,从而能够有效地杜绝非授权的系统资源和数据访问。同时通过扩展,可以支持HTTPS、SSL和TLS安全协议,已经全面支持IPv6,并有独特的Throttling功能,可以根据需要限制某些URL和URL组的服务输出量。thttpd对于并发请求采用多路复用技术实现,效能好。
thttpd全面支持HTTP 1.1(RFC2616)、CGI 1.1、HTTP基本验证(RFC2617)、虚拟主机及支持大部分的SSI(Server Side Include)功能,并能够采用PHP脚本语言进行服务器端CGI编程。有基于URL的文件流量限制,对于下载的流量控制非常方便。thttpd能够在几乎所有的Unix系统上和已知的操作系统上编译运行。
③ Mini_httpd:
Mini_httpd是一种小型的开源HTTP服务器,也是ACME Labs开发的,官网www.acme.com/software/mini_httpd/,功能不强,但很适合中小访问量的站点。
Mini_httpd实现了HTTP服务器的所有基本功能,相对比较适合学习、实验使用,支持静态网页和CGI,能够用来放置一些个人简单的东西,但不易生产使用。
④ Mongoose:
Mongoose的前身是shttp,完全开源和自由使用,官网code.google.com/p/mongoose/。Mongoose可以嵌入到其他应用程序中,为其提供Web接口,不需要配置文件,因此是较为理想的Web Server开发原型,可以基于Mongoose开发自己的Web Server。
Mongoose支持Windows、Mac、Unix、iPhone、Android和很多操作系统,具有很好的跨平台特性,支持CGI、SSL、SSI、Digest(MD5)认证,支持WebSocket和WebDAV,对SQLite 3具有很好的支持,支持断点续传、URL重写、基于IP的ACL和Windows服务,支持GET、POST、HEAD、PUT、DELETE方法,支持下载速度限制,支持基于客户端子网和URI模式。
Mongoose体积小,在Linux 2.6 i386系统上可执行文件只有64kB,具有简单清晰的API(mongoose.h),只需要一个源码文件mongoose.c,Github为github.com/valenok/mongoose/blob/master/mongoose.c。
⑤ Lighttpd:
Lighttpd是一个开源软件,官网www.lighttpd.net。Lighttpd具有非常低的内存开销,CPU占用率低,效能好,有丰富的模块,支持CGI、FastCGI、Auth、输出压缩Output Compress、URL重写、Alias等功能。Lighttpd采用Multiplex技术,代码经过优化,体积非常小,资源占用很低,反应速度快。Lighttpd适合静态资源类的服务,如图片、资源文件、静态HTML等的运用,同时也适合简单的CGI应用场合,可以很方便通过FastCGI支持PHP。Lighttpd支持的操作系统为Unix、Linux、Solaris和FreeBSD,使用内存比其他嵌入式Web服务器要多。
⑥ AppWeb:
AppWeb是一个快速、低内存使用量、标准库和方便的服务器,最大特点是功能多和高度安全。AppWeb有两种许可,一种是GPL,免费的;另一种是商业许可,有30天试用期。免费版本在www.appwebserver.org下载,商业版本由Mbedthis公司发布和维护,网址www. mbedthis.com。支持的操作系统有Linux、Windows、Mac OS X和Solaris。
⑦ Apache:
Apache是目前Internet中使用最多的Web服务器软件,官网httpd.apache.org。Apache是一个多进程Web服务器,使用内存较多,嵌入式系统中很少使用。

8. 使用CGI:

公共网关接口CGI(Common Gateway Interface)是一种编程接口,只要按照该接口的标准编写的程序,即可叫CGI程序。CGI程序的输入/输出使用编程语言的标准输入/输出,所以用C/C++来写CGI就像写普通程序一样。
当有数据从浏览器传到Web服务器后,该服务器会根据传送的类型GET/POST将这些接收到的数据传入QUERY_STRING或变量中,CGI程序可以通过标准输入在程序中接收这些数据。当要向浏览器发送信息时,只要向Web服务器发送特定的文件头信息,即可通过标准输出将信息发往Web服务器,Web服务器处理完这些由CGI程序发来的信息后,就会将这些信息发送给浏览器。这就是CGI的通信方式。
用GET方式接收到的数据保存在Web服务器的QUERY_STRING变量中,而通过POST方式接收到的数据是保存在Web服务器的变量中。以GET方式接收的数据是有长度限制的,而用POST方式接收的数据没有长度限制。以GET方式发送数据,可以通过URL形式发送,但POST方式发送数据必须通过Form才能发送。

9. SQLite数据库:

SQLite实现了一个独立的、无服务、零配置、面向事务的SQL数据库引擎。SQLite是遵守ACID的关联式数据库管理系统,设计目标是嵌入式系统,占用资源非常低,在嵌入式系统中只需要几百kB内存就够,支持Windows、Linux、Unix等主流操作系统,也能与很多编程语言结合,如Tcl、C、C#、PHP、Java等,还可以使用ODBC接口。
SQLite允许多个进程同时打开数据库,并同时对一个数据库进行读操作,当有任何进程想要对一个数据库进行写操作时,必须在更新过程中锁住数据库文件,但通常只耗费几毫秒时间。其他进程只需要等待写进程的操作结束后,继续执行它们自己的操作。

10. 网络分析工具Wireshark:

Wireshark是一个网络分析器,用来捕获数据包,并尽可能详细地显示数据包的内容,可能是最好的开源网络分析器。官网www.wireshark.org。
Wireshark支持Unix、Linux、Windows平台,能显示数据包详细的协议信息,可以存储和打开所获得的数据包,可在大量其他捕获软件之间导入和导出数据包,可使用许多标准过滤和查找数据包,能基于过滤器着色显示数据包,并创建统计信息。

11. Qt编程:

Linux下有很多可供使用的GUI库,常见的有Qt、Gtk、MiniGUI、MicroWindow等。其中,Qt是跨平台的开发库,主要用于开发图形用户界面GUI应用程序,当然也可以开发非图形的命令行应用程序。Qt支持众多的操作系统平台,本身包含的模块也日益丰富。
Qt是基于C++编程语言的GUI工具包,包括几百个C++类,速度快,有很好的移植性。在Linux系统中开发Qt程序,需要X11桌面环境的Qt集成开发环境,要安装g++编译器。从官网下载Qt安装包,Qt_SDK_Lin64_offline_v1_1_3_en.run,复制到主文件夹,双击运行安装。配置交叉编译环境,要从官网下载源码包qt-everywhere0opensource-src-4.7.4.tar.gz,复制到用户主文件夹并解压源码包。
  ·  配置并编译Qt4.7.4,默认安装路径/usr/local/Trolltech/QtEmbedded-4.7.4-arm:
  $./configure -opensource -embedded arm -xplatform qws/linux-arm-g++ -no-webkit -qt-libtiff -qt-libmng -nomake examples -nomake demos -qt-libjpeg -qt-mouse-linuxput -no-qt3support
  ·  软件编译:make
  ·  软件安装路径:/usr/local/Trolltech/QtEmbedded-4.7.4-arm
  $sudo make install
  ·  新建一个文件设置临时环境变量:
  export QTDIR=/usr/local/Trolltech/QtEmbedded-4.7.4-arm
  export QT_QWS_FONRDIR=/usr/local/Trolltech/QtEmbedded-4.7.4-arm/lib/fonts
  export QMAKEDIR=$QTDIR/qmake
  export LD_LIBRARY_PATH=$QTDIR/lib:$LD_LIBRARY_PATH
  export PATH=$QMAKEDIR/bin:$QTDIR/bin:/usr/local/arm/4.7.4/usr/bin:$PATH
  export QMAKESPEC=qws/linux-arm-g++
  export QT_SELECT=qt-4.7.4-arm

  ·  保存并退出后让环境变量生效
  ·  使用qmake -v验证
  ·  交叉编译qmake及make
  ·  在开发平台设置Qt Embedded环境
复制库文件、字体、插件库,然后设置环境变量。在根文件rootfs文件系统etc/profile文件末尾添加内容:
  export QTDIR=/usr/local/Trolltech/QtEmbedded-4.7.4-arm
  export QT_QWS_FONRDIR=$QTDIR/lib/fonts/
  export QT_PLUGIN_PATH=$QTDIR/plugins
  export LD_LIBRARY_PATH=$QTDIR/lib:$LD_LIBRARY_PATH
  export QWS_MOUSE_PROTO="LinuxInput:/dev/input/event0"

12. Android应用开发:

首先需要在计算机上搭建Java开发环境,安装好JDK、Eclipse、ADT、Android SDK、Android NDK。JDK是Java语言的软件开发工具包,主要用于移动设备、嵌入式设备上的Java应用程序,这是Java开发的核心,包含了Java的运行环境、工具和基础类库。Eclipse是一种集成开发环境IDE,有支持多种语言的插件。Eclipse ADT是Eclipse平台下开发Android应用程序的插件,用于打包和封装Android应用。
Android SDK是Android专属软件开发工具包,开发出的软件靠Dalvik虚拟机来运行;NDK是C语言软件包,一般只能在特定的CPU指令集的机器上运行,用来开发驱动或底层应用。

1) LED控制示例:

Android应用层开发由Java语言实现,然后加载C语言组件动态库。C语言组件动态库必须以JNI框架标准编写,在Java层实现主类和native类,主类用于控制LED,native类主要与底层C交互。native类定义3个本地方法,分别是打开led、控制led和关闭led。在C语言组件动态库调用led驱动程序。
  public class MainActivity extends AppCompatActivity{
     private LedModule led;
     private ToggleButton bedroom,parlour;
     private boolean bedroom_state=false,parlour_state=false;
     @Override
     protected void onCreate(Bundle savedInstanceState){
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       led=LedModule.led_mk();
       led.openLED();
       parlour=(ToggleButton) findViewById(R.id.parlour);
       bedroom=(ToggleButton) findViewById(R.id.bedroom);
       parlour.setOnClickListener(new OnClickListener(){
         @Override
         public void onClick(View v){
           if(parlour_state){
             led.ctlLED(0,0);
             parlour_state=false;
           }else{
             led.ctlLED(0,1);
             parlour_state=true;
           }
         }
       });
       bedroom.setOnClickListener(new OnClickListener(){
         @Override
         public void onClick(View v){
           if(bedroom_state){
             led.ctlLED(1,0);
             bedroom_state=false;
           }else{
             led.ctlLED(1,1);
             bedroom_state=true;
           }
         }
       });
     }
     protected void onDestroy(){
       super.onDestroy();
       led.closeLED();   //释放被占用的LED设备
     }
  }

LedModule.class源代码,其中封装着实现JNI的native函数:
  public class LedModule{
     private static LedModule led;
     static{
     System.loadLibrary("LED");   //静态加载C组件动态库libLED.so
     }
     public native void openLED();   //打开LED字符设备节点
     public native void ctlLED(int num,int ctl);   //控制LED
     public native void closeLED();   //关闭LED字符设备节点
     public static LedModule led_mk(){   //保证只能有一个实例
     if(led==null)
       led=new LedModule();
     }
     return led;
     }
  }

LedModule类是Led控制的JNI,首先通过System.loadLibrary函数加载底层so库,然后才能调用打开、关闭和控制Led。
工程中新建jni目录,编写C组件动态库的LED.c文件,这是控制硬件的代码,也是C函数:
  #include <jni.h>
  #include <sys/ioctl.h>
  #include <fcntl.h>
  int fd=-1;
  JNIEXPORT void JNICALL Java_com_gec_led_LedModule_openLED(JNIEnv *env,jobject obj){
     fd=open("/dev/Led",O_RDWR);
  }
  JNIEXPORT void JNICALL Java_com_gec_led_LedModule_ctlLED(JNIEnv *env,jobject obj,jint num,jint status){
     ioctl(fd,num,status);
  }
  JNIEXPORT void JNICALL Java_com_gec_led_LedModule_closeLED(JNIEnv *env,jobject obj){
     ioctl(fd,0,0);
     ioctl(fd,1,0);
     close(fd);
  }

根据JNI1.0调用规则,在Java代码中调用native函数,当Java上层调用openLed的native函数时,实质是执行了C语言LED.c下的Java_com_gec_led_LedModule_openLED函数。
编写生成JNI动态库的配置文件Android.mk,用于记录要编译的C函数文件以及一些设置:
  LOCAL_PATH := $(call my-dir)
  include $(CLEAR_VARS)
  LOCAL_MODULE:=libLED
  LOCAL_SRC_FILES:=LED.c
  LOCAL_C_INCLUDES+=\
  $(JNI_H_INCLUDE)
  LOCAL_PRELINK_MODULE:=false
  LOCAL_MODULE_TAGS:=eng
  include $(BUILD_SHARED_LIBRARY)

上述代码包括内容:生成C动态库名称libLED,Java层通过加载此库名称来实现互调;编译C文件;加载JNI库头文件;Prelink利用事先连接代替运行时连接的方法加快共享库的加载,如果变量设置false将不做prelink操作;该模块只在eng版本下才编译;生成libLED动态库。

2) A/D控制示例:

AdcModule.class源代码中封装着可调用JNI的native函数,用于打开和关闭ADC驱动,获取ADC数据并回传到主界面。其中SendMes函数封装着int类型的数据,从底层回调到Java上层,再传到Handler类处理。
  public class AdcModule{
     private static AdcModule adc;
     public Handler handler;
     static{
       System.loadLibrary("ADC");   //静态加载C组件动态库libADC.so
     }
     public native void openADC();   //打开LED字符设备节点
     public native void closeADC();   //关闭LED字符设备节点
     public static LedModule adc_mk(){   //保证只能有一个实例
       if(adc==null)
         adc=new AdcModule();
       }
       return adc;
     }
     //将已经初始化好的handler与这个类关联起来
     public void setHandler(Handler handler){
       this.handler=handler;
     }
     //把数据通过handler传到界面显示
     public void SendMes(int one){
       if(handler!=null){
         Message msg=new Message();
         msg.obj=one;
         handler.sendMessage(msg);
       }
     }
  }

Java中调用的部分代码:
  //定义handler变量,接收ADC数据消息
  handler = new Handler(){
     @Override
     public void handleMessage(Message msg){
       text.setText(String.valueOf((Integer) msg.obj));
     }
  };
  adc=AdcModule.adc_mk();   //获取AdcModule实例
  adc.setHandler(handler);   //将handler与AdcModule关联
  adc.openADC();   //获取ADC设备并开始读取数据

在工程目录下新建jni目录,编写组件动态库ADC.c文件。
需要很多头文件:
  #include <jni.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <unistd.h>
  #include <string.h>
  #include <strings.h>
  #include <time.h>
  #include <signal.h>
  #include <termios.h>
  #include <sys/stat.h>
  #include <sys/types.h>
  #include <fcntl.h>
  #include <sys/ipc.h>
  #include <pthread.h>
  #include <errno.h>
  #include <semaphore.h>
  #include <sys/ioctl.h>
  #include <android/log.h>
  #include <sys/select.h>
  #include <netdb.h>

代码:
  int run=0;
  int fd=-1;
  JavaVM *g_jvm=NULL;
  jobject *g_obj=NULL;
  int isEnable=1;
  void *get_adc_main(void *arg){
     char counter[30];
     int ret;
     int value;
     JNIEnv *env;
     jclass cls;
     jmethodID mid;
     if((*g_jvm)->AttachCurrentThread(g_jvm,&env,NULL)!=JNI_OK){
       return NULL;
     }
     //找到对应的类
     cls=(*env)->GetObjectClass(env,g_obj);
     while(isEnable){
       value=-1;
       ret=read(fd,&value,4);
       jmethodID methodId=(*env)->GetMethodID(env,cls,"SendMes","(I)V");
       (*env)->CallVoidMethod(env,g_obj,methodId,value);
       usleep(75000);
     }
     (*g_jvm)->DetachCurrentThread(g_jvm);
  }
  JNIEXPORT void JNICALL Java_com_gec_adc_AdcModule_openADC(JNIEnv *env,jobject obj){
     fd=open("/dev/adc",O_RDONLY);
     (*env)->GetJavaVM(env,&g_jvm);
     g_obj= (*env)->NewGlobalRef(env,obj);
     isEnable=1;
     pthread_t tid;
     pthread_create(&tid,NULL,get_adc_main,NULL);
  }
  JNIEXPORT void JNICALL Java_com_gec_adc_AdcModule_closeADC(JNIEnv *env,jobject obj){
     isEnable=0;
     close(fd);
  }

生成JNI动态库的配置文件Android.mk:
  LOCAL_PATH := $(call my-dir)
  include $(CLEAR_VARS)
  LOCAL_MODULE:=libADC
  LOCAL_SRC_FILES:=ADC.c
  LOCAL_C_INCLUDES+=
  $(JNI_H_INCLUDE)
  LOCAL_PRELINK_MODULE:=false
  LOCAL_MODULE_TAGS:=debug
  include $(BUILD_SHARED_LIBRARY)

设置内容,生成C动态库名libADC,Java层通过加载此库名称实现互调;编译C文件;加载JNI库头文件;生成libadctest动态库,要与Java静态加载的库名一致。

3) LCD示例:

LcdModule类用来操作底层C语言函数,有3个native函数,用于打开底层驱动、关闭驱动和显示开始刷屏幕。
  public class AdcModule{
     static{
       System.loadLibrary("lcd");   //静态加载C组件动态库liblcd.so
     }
     public static native void openLCD();   //打开驱动
     public static native void showLCD();   //显示LCD刷屏
     public static native void closeLCD();   //关闭驱动
  }

Java中调用的部分代码:
  LcdModule.openLCD();   //打开驱动
  lcdBt=(Button) this.findViewById(R.id.lcdbt);
  lcdBt.setOnClickListener(new OnClickListener()){
     @Override
     public void onClick(View v){
       new Thread(new Runnable(){
         @Override
         public void run(){
           while(isRun){
             try{
               Thread.sleep(1500);
               LcdModule.showLCD();
               Thread.sleep(3500);
             }catch(InterruptedException e){
               e.printStackTrace();
             }
           }
         }
       }).start();
     }
  });

在工程目录下新建jni目录,编写组件动态库LCD.c文件。
头文件:
  #include <jni.h>
  #include <sys/types.h>
  #include <sys/stat.h>
  #include <fcntl.h>
  #include <android/log.h>
  #include <linux/fb.h>
  #include <sys/mman.h>
  #include <pthread.h>
  //常量与变量
  #define LOG_NDEBUG 0
  int fd=-1;
  struct fb_var_screeninfo vinfo;
  struct fb_fix_screeninfo finfo;
  long int screensize=0;
  unsigned long *fbp=NULL;
  int x=0,y=0,z=0;
  int isrun=0;
  const unsigned long colors[]={0x07FF0000,0xF81FFFFFF,0xFFE0F11F,0x00FFF000,0xFF0000FF,0xF800FF00,0x001F00FF,0x07E0FFFF};
  //显示函数
  void *show(void *arg){
     for(z=0;z<sizeof(colors)/sizeof(unsigned long);z++){
       usloop(400000);
       vinfo.xoffset=0;
       vinfo.yoffset=0;
       for(y=0;y<vinfo.yres;y++){
         for(x=0;x<vinfo.xres;x++){
           *(fbp+y*vinfo.xres+x)=colors[z];
         }
       }
     }
  }
  JNIEXPORT void JNICALL Java_com_gec_adc_AdcModule_openLCD(JNIEnv *env,jobject obj){
     fd=open("/dev/graphics/fb0",O_RDWR);
  }
  JNIEXPORT void JNICALL Java_com_gec_adc_AdcModule_showLCD(JNIEnv *env,jobject obj){
     ioctl(fd,FBIOGET_VSCREENINFO,&vinfo);
     vinfo.bits_per_pixel=32;
     ioctl(fd,FBIOPUT_VSCREENINFO,&vinfo);
     ioctl(fd,FBIOGET_VSCREENINFO,&finfo);
     ioctl(fd,FBIOGET_VSCREENINFO,&vinfo);
     screensize=vinfo.xres*vinfo.yres*vinfo.bits_per_pixel/8;
     fbp=(unsigned long *)mmap(0,screensize,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
     isrun=1;
     pthread_t tid;
     pthread_create(&tid,NULL,get_adc_main,NULL);
  }
  JNIEXPORT void JNICALL Java_com_gec_adc_AdcModule_closeLCD(JNIEnv *env,jobject obj){
     isrun=0;
     munmap(fbp,screensize);
     close(fd);
  }

生成JNI动态库的配置文件Android.mk:
  LOCAL_PATH := $(call my-dir)
  include $(CLEAR_VARS)
  LOCAL_MODULE:=liblcd
  LOCAL_SRC_FILES:=LCD.c
  LOCAL_C_INCLUDES+=\
     $(JNI_H_INCLUDE)
  LOCAL_PRELINK_MODULE:=false
  LOCAL_MODULE_TAGS:=debug
  include $(BUILD_SHARED_LIBRARY)

Copyright@dwenzhao.cn All Rights Reserved   备案号:粤ICP备15026949号
联系邮箱:dwenzhao@163.com  QQ:1608288659