嵌入式linux设备驱动程序的开发与应用

时间:2022-10-18 12:10:28

嵌入式linux设备驱动程序的开发与应用

【摘要】该文对Linux设备驱动程序原理进行了分析,对模块化的概念进行了阐述,并具体分析了字符设备驱动程序的构成:包括重要的头文件及重要的数据结构,以及Makefile的编写。最后搭建平台以SPI驱动程序的开发为例说明开发驱动程序的具体过程。

【关键词】设备驱动;模块,Linux; SPI;嵌入式系统

Linux系统是一个免费使用的类似UNIX的操作系统,因具有设备独立性而被移植到数十种处理器上。Linux不仅仅支持丰富的硬件设备、文件系统,更主要的是它提供了完整的源代码和开发工具。所以越来越多的嵌入式选择linux作为其操作系统。在嵌入式linux系统的开发过程中,大量新硬件的问世,为linux驱动程序的开发提供了必要条件。

1.设备驱动程序原理

驱动程序用来沟通硬件和应用软件,充当了二者之间的纽带,使得应用软件仅仅调用系统软件的应用编程接口即API就可以让硬件去完成要求的工作。设备驱动用来驱动硬件设备,它与底层硬件直接打交道,根据硬件的具体工作方式读写设备寄存器,完成设备的轮询、中断处理、DMA通信、实现物理内存到虚拟内存的映射,来实现对硬件的操作(使通信设备能够收发数据、显示设备能够显示界面、存储设备能够存储文件和数据)。

设备驱动程序针对的对象主要是包括CPU内部集成的存储器和外设在内的各种设备,针对不同的设备,linux将驱动程序分为3类:字符设备驱动、块设备驱动、网络设备驱动。

字符设备是指那些需要以串行顺序依次进行访问的设备,如鼠标、触摸屏等。可以像访问文件一样访问字符设备,而字符设备驱动通过实现open()、close()、read()、write()等函数负责实现这些行为。字符设备文件和普通文件之间的唯一差别在于对普通文件的访问可以不按照顺序访问,而大多数字符设备是一个只能顺序访问的数据通道。

块设备可用任意顺序进行访问,它是文件系统的宿主。块设备驱动程序经过系统的快速缓冲以块为单位进行操作,如磁盘、软驱等。字符设备和块设备并没有明显的区别,它们只是在内核内部的管理上有所不同,即内核和驱动程序间的接口上有所区别。

网络设备不是面向流的设备,是面向数据包的接收和发送而设计。任何网络事件的操作都是通过接口实现的,而接口是一个硬件,网络接口是由网络内核子系统驱动的,它负责发送和接收数据包。内核和网络设备的通信与字符设备和块设备和内核之间的通信方式完全不一样。

字符设备驱动程序最为简单,但这类设备驱动程序适用于大多数的硬件设备,并且其中的一些方法同样适用于块设备和网络设备,因此本文将着重分析字符设备驱动程序的结构和开发。

2.模块化的相关概念

linux有一个很好的特点:内核提供的特性可以在运行时进行扩展,即在系统启动并运行的时候,我们可以向内核添加或者移除功能。在系统运行时向内核里添加的实现某功能的代码被称为“模块”。Linux中的可加载模块(module)是linux内核支持的动态可加载模块(loadablemodule),它们是核心的一部分,而这部分往往是设备驱动程序,但是他们并没有被编译到内核中。模块由目标代码组成,是个目标文件。在需要时使用insmod或者modprobe加载模块,不需要时使用rmmod卸载模块。通过动态地将代码载入 核心可以减小核心代码的规模,是核心配置更为灵活,并极大地方便了用户的调试。

在执行insmod命令加载模块时,驱动程序中的init_module函数被调用;在执行rmmod命令卸载模块时,驱动程序中的cleanup_module函数被调用。模块成功加载后,系统将为该驱动程序分配一个主设备号,我们就可以利用这个设备号建立(删除)设备文件了:

mknod: mknod/dev/设备名 c/-b 主设备号(MAJOR) 次设备号(MINOR)

同样,删除设备文件使用如下命令:

rmnod/dev/设备文件名

其中-c代表建立的是字符设备,-b代表建立的是块设备,次设备号由自己决定。使用同一个驱动程序进行驱动的设备,它们的主设备号相同,用次设备号的不同来区别使用同一驱动的不同设备。

模块许可证:LICENSE描述了内核模块的许可权限,模块被加载时,若不声明,将会收到内核被污染的警告。

3.字符设备驱动程序的构成

1)有用的头文件

#include <linux/module.h>

//包含可加载模块需要的大量符号和函数的定义,必须包含在模块的源代码中

#include <linux/init.h>//指定初始化和清除函数

module_init(init_function);

module_exit(cleanup_function);

MODULE_LICENSE(“GPL”);//指定代码所使用的许可证

#include <linux/sched.h>//包含驱动程序所使用的大部分内核API的定义,如睡眠函数的声明等各种变量声明

#include <linux/version.h>//包含所构造内核版本信息的头文件

#include <linux/kernel.h>//内核代码

#include <moduleparam.h>//用来创建模块参数的宏,在装载时向模块传递参数

2)重要的数据结构

Linux使用file_operations结构访问驱动程序的函数,这个结构的每一个成员的名字都对应着一个系统调用。上面我们已经建立了一些设备编号,但并没有将这些操作与任何驱动程序相连。而file_operations结构就是用来建立这种连接的。这个结构在<linux/fs.h>中定义,它包含了一组函数指针。

用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。所以file_operations结构体中的成员函数是字符设备驱动程序设计的主题内容,这些函数实际会在应用程序进行Linux的open()、close()、read()、write()等系统调用时最终被调用。由此,编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域

file_operations结构的定义基本如下:

struct fileoperations

{

struct module *owner;

//拥有该结构的模块的指针

loff_t (*llseek) (struct file*,loff_t,int);

//方法llseek用来修改文件当前的读写位置

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);

//仅用于读取目录。对设备文件来说,该字段应该为NULL

unsigned int (*poll) (struct file *,struct poll_table_struct *);

//返回一个位掩码,用来指出非阻塞的读取或写入是否可能

int (*ioctl) (struct inode *,struct file *,unsigned int,unsigned long);

//执行设备IO控制命令

int (*mmap) (struct file *,struct vm_area_struct *);

//用于请求将设备内存映射到进程地址空间

int (*open) (struct inode *,struct file *);

//打开设备文件

int (*flush) (struct file *);

int (*release) (struct inode *,struct file *);

//当file结构释放时,调用该操作

int (*fsync) (struct file *,struct dentry *,int);

//调用它来刷新待处理的数据

int (*aio_fsync) (struct kiocb *,int);

//fsync的异步版本

int (*fasync) (int,struct file *,int);

//通知设备其fasync标志发生了变化

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 *);

//实现sendfile系统调用的读取部分,通常为NULL

ssize_t (sendpage) (struct file *,struct page *,int,size_t,loff_t *,int);

//是sendfile系统调用的另一半,通常为NULL

unsigned long (*get_unmapped_area) (struct file *,unsigned long,unsigned long,unsigned long,unsigned long);

//在进程的地址空间中找到一个将底层设备中的内存映射的位置

int (check_flags) (int);

//允许模块检查传递给fcntl调用的标志

int (*dir_notify) (struct file *,unsigned long);

//驱动程序不必实现,仅对文件系统有效

字符设备驱动程序的file_operations结构被初始化为如下形式:

struct file_operations xxx_fops=

{

.owner =THIS_MODULE,

.llseek = xxx_llseek,

.read = xxx_read,

.write = xxx_write,

.ioctl = xxx_ioctl,

.open = xxx_open,

.release = xxx_release,

};

4.字符设备驱动应用举例

本文将以基于Hi3510和DSP2812的SSP(SPI)通信驱动程序为例,分析字符设备驱动程序的实现过程。Hi3510 是一款基于ARM9、DSP双处理器内核以及硬件加速引擎的高集成、可编程、支持MPEG-4 AVC/H.264等多协议的高性能通信媒体处理器芯片,可广泛应用于实时视频通信、数字图像监控等领域。

DSP2812是TI公司新推出的功能强大的TMS320F2812的32位定点DSP,最大的特点是速度有了质的飞跃,从最高40M跃升到150M,处理数据位数也从16位定点跃升到32位定点,并且拥有EVA、EVB事件管理器和配套的12位16通道的AD数据采集,丰富的外设接口,如CAN、SCI等,在工控领域占有不少份额。实现二者之间的SPI通信具有重要的现实意义。

SPI 用于CPU与各种器件进行全双工、同步串行通讯。它只需四条线就可以完成MCU与各种器件的通讯,节约了芯片的管脚,同时为PCB的布局上节省空间,这四条线是:串行时钟线(CLK)、主机输入/从机输出数据线(MISO)、主机输出/从机输入数据线(MOSI)、低电平有效从机选择线CS。SPI主要特点有:可以同时发出和接收串行数据;可以当作主机或从机工作;提供频率可编程时钟;发送结束中断标志;写冲突保护;总线竞争保护等。

Hi3510和DSP2812的接口电路如图所示:

1)SSP驱动程序的实现

其基本框架如下:

重要的头文件

#include <linux/module.h>

#include <linux/config.h>

#include <linux/miscdevice.h>

#include <linux/init.h>

#include <linux/proc_fs.h>

#include <asm/io.h>

#include “hi_ssp.h”

MODULE_LICENSE(“GPL”);对模块进行声明

用int init_module()检查外设是否存在、初始化该设备用到的硬件和数据结构、注册设备及向系统申请中断;cleanup_module()用来实现对硬件清除、释放内存和中断,并注销设备。这里主要是file_operations结构的填充,实现驱动程序的过程就是实现各个read()、write()、open()、close()等接口函数的过程。各接口函数实现如下:

int ssp_open(struct inode *,struct file *);

int ssp_release(struct inode *,struct file *);

ssize_t ssp_read(struct file * file, char __user * buf, size_t count, loff_t * offset);

ssize_t ssp_write(struct file * file, const char __user * buf, size_t count, loff_t * offset);

int ssp_ioctl(struct inode *,struct file *,unsigned int,unsigned long);

同样,file_operations的结构体如下:

static struct file_operations ssp_fops =

{

.owner = THIS_MODULE,

.read = ssp_read,

.write = ssp_write,

.ioctl = ssp_ioctl,

.open = ssp_open,

.release = ssp_close

};

用户利用SPI接口发送和接收数据时,系统分别调用ssp_write和ssp_read函数。ssp_write通过内核函数copy_from_usr接收用户需要发送的数据,ssp_read通过copy_to_user将接收的数据传送给用户。通过ssp_open进行初始化,完成数据传输之后通过ssp_release释放设备。

2)对源代码进行编译

该操作的重点在于Makefile的编写。

本文中的Makefile编写如下(本试验台中LINUXROOT的路径如下):

LINUXROOT := /hisilicon/Hi3510_VSSDK_V1.3.3.0_A01/code/linux/kernel/linux-2.6.14

TOPDIR = .

CFLAGS += -I$(PWD)/include

include $(PWD)/dirs.mk

default:

make -C $(LINUXROOT) M=$(PWD) modules

clean:

rm *.ko *.mod.c .*.ko.cmd *.o.cmd .*.mod.*.* -fv

rm .tmp_versions -frv

将Makefile文件与源代码放入同一文件夹,然后执行make,即可得到以.ko为后缀的模块。

通过chmod 777更改模块权限,使之成为可执行模块。

3)通过insmod命令加载模块

并通过如下命令创建文件节点:

Mknod /dev/hi_ssp c major minor

c是指加载的设备是字符设备,本实验中主设备号是10,次设备号是0。

执行命令lsmod查看,发现在Module下面已经有hi_ssp模块存在。

到这里,驱动程序已经实现了。在该驱动模块的基础上编写相应的应用程序,使之完成对各个接口函数的调用,即可实验Hi3510和DSP2812之间的SPI通信。

4)ARM与DSP之间SPI通信的实现

在Hi3510与DSP2812之间实现数据传输,只需要4根线,即DI,DO,CLK,CS。2812与spi相关的引脚分布图如图1所示。

图1 Hi3510与DSP2812之间通讯原理图

图2 SSP的3种串行口模式

SSP可以配置成3种串行口模式,分别是TI SPI模式、Motorola SPI模式和National Semiconductor Micro Ware模式。这里的接口时序采用Motorola模式。其接口时序如图2所示。

5.结论

本文用上位机LabVIEW进行了验证,即采用“传感器+DSP2812+Hi3510+LabVIEW”模式进行数据传输和显示,将Hi3510的DI、DO、CLK、CS分别连接2812的DO、DI、CLK和CS。各部分的ip配置在同一网段内,向Hi3510中加载模块,并编写一个spi读程序。Spi读程序框架如下

#include <stdlib.h>

#include <stdio.h>

#include <sys/ioctl.h>

……//包含相应的头文件

ssp_open ()//调用驱动中的接口函数open()

ssp_read (char *buffer, int length)// 调用驱动中的接口函数read()

ssp_close ()//调用驱动中的接口函数close()

main(){}

图3 上位机LabVIEW通过SPI接收的数据流

用交叉编译工具对spi.c进行编译,在挂载目录下运行spi.c编译成的二进制文件spi,在上位机LabVIEW下接收的数据图如图3所示。

参考文献

[1]Mait Welsh,Matthias KalleDalheimer,Lar Kaufman,Linux权威指南(第三版)[M].中国电力出版社,2000.

[2]Alessandro Rubini,Jonathan Corbet.Linux Device Drivers 2.O’Reilly[M].2002:89-101.

[3]Maxim. New Release DataBook[J].1997(1).

[4]Intel PXA27x Processor Family Develop’s Manual[EB/OL].http://.2004.

上一篇:基于物联网的智能家居照明系统 下一篇:采用无线单片机的高压供电监测器设计