Linux下的C编程实战(五)――驱动程序设计

Linux 下的 C 编程实战(五) <?XML:NAMESPACE PREFIX = O />

――驱动程序设计

1. 引言

设备驱动程序是操作系统内核和机器硬件之间的接口,它为应用程序屏蔽硬件的细节,一般来说, Linux 的设备驱动程序需要完成如下功能:

1 )初始化设备;

2 )提供各类设备服务;

3 )负责内核和设备之间的数据交换;

4 )检测和处理设备工作过程中出现的错误。

妙不可言的是, Linux 下的设备驱动程序被组织为一组完成不同任务的函数的集合,通过这些函数使得 Windows 的设备操作犹如文件一般。在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。本系列文章的第 2 章文件系统编程中已经看到了这些函数的真面目,它们就是 open () close () read () write () 等。

Linux 主要将设备分为二类:字符设备和块设备(当然网络设备及 USB 等其它设备的驱动编写方法又稍有不同)。这两类设备的不同点在于:在对字符设备发出读 / 写请求时,实际的硬件 I/O 一般就紧接着发生了,而块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的 I/O 操作。块设备主要针对磁盘等慢速设备。以字符设备的驱动较为简单,因此本章主要阐述字符设备的驱动编写。

2. 驱动模块函数

init 函数用来完成对所控设备的初始化工作,并调用 register_chrdev() 函数注册字符设备。假设有一字符设备“ exampledev ”,则其 init 函数为:

void exampledev_init(void)

{

  if (register_chrdev(MAJOR_NUM, " exampledev ", &exampledev_fops))

    TRACE_TXT("Device exampledev driver registered error");

  else

    TRACE_TXT("Device exampledev driver registered successfully");

  …// 设备初始化

}

其中, register_chrdev 函数中的参数 MAJOR_NUM 为主设备号 , exampledev ”为设备名, exampledev_fops 为包含基本函数入口点的结构体,类型为 file_operations 。当执行 exampledev_init 时,它将调用内核函数 register_chrdev ,把驱动程序的基本入口点指针存放在内核的字符设备地址表中,在用户进程对该设备执行系统调用时提供入口地址。

file_operations 结构体定义为:

struct file_operations

{

  int (*lseek)();

  int (*read)();

  int (*write)();

  int (*readdir)();

  int (*select)();

  int (*ioctl)();

  int (*mmap)();

  int (*open)();

  void(*release)();

  int (*fsync)();

  int (*fasync)();

  int (*check_media_change)();

  void(*revalidate)();

};

大多数的驱动程序只是利用了其中的一部分,对于驱动程序中无需提供的功能,只需要把相应位置的值设为 NULL 。对于字符设备来说,要提供的主要入口有: open () release () read () write () ioctl ()

open() 函数  对设备特殊文件进行 open() 系统调用时,将调用驱动程序的 open () 函数:

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

其中参数 inode 为设备特殊文件的 inode ( 索引结点 ) 结构的指针,参数 file 是指向这一设备的文件结构的指针。 open() 的主要任务是确定硬件处在就绪状态、验证次设备号的合法性 ( 次设备号可以用 MINOR(inode-> i - rdev) 取得 ) 、控制使用设备的进程数、根据执行情况返回状态码 (0 表示成功,负数表示存在错误 ) 等;

release() 函数  当最后一个打开设备的用户进程执行 close () 系统调用时,内核将调用驱动程序的 release () 函数:

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

release 函数的主要任务是清理未结束的输入 / 输出操作、释放资源、用户自定义排他标志的复位等。

read() 函数  当对设备特殊文件进行 read() 系统调用时,将调用驱动程序 read() 函数:

void read(struct inode * inode ,struct file * file ,char * buf ,int count) ;

参数 buf 是指向用户空间缓冲区的指针,由用户进程给出, count 为用户进程要求读取的字节数,也由用户给出。

read() 函数的功能就是从硬设备或内核内存中读取或复制 count 个字节到 buf 指定的缓冲区中。在复制数据时要注意,驱动程序运行在内核中,而 buf 指定的缓冲区在用户内存区中,是不能直接在内核中访问使用的,因此,必须使用特殊的复制函数来完成复制工作,这些函数在 <asm/ segment.h> 中定义:

void put_user_byte (char data_byte ,char * u_addr) ;

void put_user_word (short data_word ,short * u_addr) ;

void put_user_long(long data_long ,long * u_addr) ;

void memcpy_tofs (void * u_addr ,void * k_addr ,unsigned long cnt) ;

参数 u_addr 为用户空间地址, k_addr 为内核空间地址, cnt 为字节数。

write( ) 函数  当设备特殊文件进行 write () 系统调用时,将调用驱动程序的 write () 函数:

void write (struct inode * inode ,struct file * file ,char * buf ,int count) ;

write () 的功能是将参数 buf 指定的缓冲区中的 count 个字节内容复制到硬件或内核内存中,和 read() 一样,复制工作也需要由特殊函数来完成:

unsigned char_get_user_byte (char * u_addr) ;

unsigned char_get_user_word (short * u_addr) ;

unsigned char_get_user_long(long * u_addr) ;

unsigned memcpy_fromfs(void * k_addr ,void * u_addr ,unsigned long cnt) ;

ioctl() 函数  该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,函数原型为:

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

参数 cmd 为设备驱动程序要执行的命令的代码,由用户自定义,参数 arg 为相应的命令提供参数,类型可以是整型、指针等。

同样,在驱动程序中,这些函数的定义也必须符合命名规则,按照本文约定,设备“ exampledev ”的驱动程序的这些函数应分别命名为 exampledev_open exampledev_ release exampledev_read exampledev_write exampledev_ioctl ,因此设备“ exampledev ”的基本入口点结构变量 exampledev_fops 赋值如下:

struct file_operations exampledev_fops {

  NULL ,

  exampledev_read ,

  exampledev_write ,

  NULL ,

  NULL ,

  exampledev_ioctl ,

  NULL ,

  exampledev_open ,

  exampledev_release ,

  NULL ,

  NULL ,

  NULL ,

  NULL

} ;

3. 内存分配

由于 Linux 驱动程序在内核中运行,因此在设备驱动程序需要申请 / 释放内存时,不能使用用户级的 malloc/free 函数,而需由内核级的函数 kmalloc/kfree () 来实现, kmalloc() 函数的原型为:

void kmalloc (size_t size ,int priority);

参数 size 为申请分配内存的字节数;参数 priority 说明若 kmalloc() 不能马上分配内存时用户进程要采用的动作: GFP_KERNEL 表示等待,即等 kmalloc() 函数将一些内存安排到交换区来满足你的内存需要, GFP_ATOMIC 表示不等待,如不能立即分配到内存则返回 0 值;函数的返回值指向已分配内存的起始地址,出错时,返回 0

kmalloc () 分配的内存需用 kfree() 函数来释放, kfree () 被定义为:

# define kfree (n) kfree_s( (n) ,0)

其中 kfree_s () 函数原型为:

void kfree_s (void * ptr ,int size);

参数 ptr kmalloc() 返回的已分配内存的指针, size 是要释放内存的字节数,若为 0 时,由内核自动确定内存的大小。

4. 中断

许多设备涉及到中断操作,因此,在这样的设备的驱动程序中需要对硬件产生的中断请求提供中断服务程序。与注册基本入口点一样,驱动程序也要请求内核将特定的中断请求和中断服务程序联系在一起。在 Linux 中,用 request_irq() 函数来实现请求:

int request_irq (unsigned int irq ,void( * handler) int ,unsigned long type ,char * name);

参数 irq 为要中断请求号,参数 handler 为指向中断服务程序的指针,参数 type 用来确定是正常中断还是快速中断(正常中断指中断服务子程序返回后,内核可以执行调度程序来确定将运行哪一个进程;而快速中断是指中断服务子程序返回后,立即执行被中断程序,正常中断 type 取值为 0 ,快速中断 type 取值为 SA_INTERRUPT ),参数 name 是设备驱动程序的名称。

5. 实例

笔者最近设计了一块采用三星 S<?XML:NAMESPACE PREFIX = ST1 />3C2410 ARM 处理器的电路板( ARM 处理器广泛应用于手机、 PDA 等嵌入式系统),板上包含四个用户可编程的发光二极管( LED ),这些 LED 连接在 ARM 处理器的可编程 I/O 口( GPIO )上。下图给出了 ARM 中央处理器与 LED 的连接原理:

<?XML:NAMESPACE PREFIX = V /> 
 
我们在 ARM 处理器上移植 Linux 操作系统,现在来编写这些 LED 的驱动:

#include <linux/config.h>

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/init.h>

#include <linux/miscdevice.h>

#include <linux/sched.h>

#include <linux/delay.h>

#include <linux/poll.h>

#include <linux/spinlock.h>

#include <linux/irq.h>

#include <linux/delay.h>

#include <asm/hardware.h>

#define DEVICE_NAME "leds" /* 定义 led 设备的名字 */

#define LED_MAJOR 231 /* 定义 led 设备的主设备号 */

static unsigned long led_table[] =

{

  /*I/O 方式 led 设备对应的硬件资源 */

  GPIO_B10, GPIO_B8, GPIO_B5, GPIO_B6,

};

/* 使用 ioctl 控制 led*/

static int leds_ioctl(struct inode *inode, struct file *file, unsigned int cmd,

  unsigned long arg)

{

  switch (cmd)

  {

    case 0:

    case 1:

      if (arg > 4)

      {

        return  -EINVAL;

      }

      write_gpio_bit(led_table[arg], !cmd);

    default:

      return  -EINVAL;

  }

}

 

static struct file_operations leds_fops =

{

  owner: THIS_MODULE, ioctl: leds_ioctl,

};

static devfs_handle_t devfs_handle;

static int __init leds_init(void)

{

  int ret;

  int i;

  /* 在内核中注册设备 */

  ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &leds_fops);

  if (ret < 0)

  {

    printk(DEVICE_NAME " can't register major number\n");

    return ret;

  }

  devfs_handle = devfs_register(NULL, DEVICE_NAME, DEVFS_FL_DEFAULT, LED_MAJOR,

    0, S_IFCHR | S_IRUSR | S_IWUSR, &leds_fops, NULL);

  /* 使用宏进行端口初始化, set_gpio_ctrl write_gpio_bit 均为宏定义 */

  for (i = 0; i < 8; i++)

  {

    set_gpio_ctrl(led_table[i] | GPIO_PULLUP_EN | GPIO_MODE_OUT);

    write_gpio_bit(led_table[i], 1);

  }

  printk(DEVICE_NAME " initialized\n");

  return 0;

}

 

static void __exit leds_exit(void)

{

  devfs_unregister(devfs_handle);

  unregister_chrdev(LED_MAJOR, DEVICE_NAME);

}

 

module_init(leds_init);

module_exit(leds_exit);

使用命令方式编译 led 驱动模块:

#arm-linux-gcc -D__KERNEL__ -I/arm/kernel/include

-DKBUILD_BASENAME=leds -DMODULE -c -o leds.o leds.c

以上命令将生成 leds.o 文件,把该文件复制到板子的 /lib 目录下,使用以下命令就可以安装 leds 驱动模块:

#insmod /lib/ leds.o

删除该模块的命令是:

#rmmod leds

6. 小结

本章讲述了 Linux 设备驱动程序的入口函数及驱动程序中的内存申请、中断等,并给出了一个通过 ARM 处理器的 GPIO 口控制 LED 的驱动实例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值