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

简介:

Linux下的C编程实战(五)

――驱动程序设计
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
kmalloc () 分配的内存需用 kfree() 函数来释放, kfree () 被定义为:
# define kfree (n) kfree_s( (n) ,0)
其中 kfree_s ()  函数原型为:
void kfree_s (void * ptr ,int size);
参数 ptr kmalloc() 返回的已分配内存的指针, size 是要释放内存的字节数,若为 时,由内核自动确定内存的大小。
4. 中断
许多设备涉及到中断操作,因此,在这样的设备的驱动程序中需要对硬件产生的中断请求提供中断服务程序。与注册基本入口点一样,驱动程序也要请求内核将特定的中断请求和中断服务程序联系在一起。在 Linux 中,用 request_irq() 函数来实现请求:
int request_irq (unsigned int irq ,void( * handler) int ,unsigned long type ,char * name);
参数 irq 为要中断请求号,参数 handler 为指向中断服务程序的指针,参数 type  用来确定是正常中断还是快速中断(正常中断指中断服务子程序返回后,内核可以执行调度程序来确定将运行哪一个进程;而快速中断是指中断服务子程序返回后,立即执行被中断程序,正常中断 type  取值为 ,快速中断 type  取值为 SA_INTERRUPT ),参数 name 是设备驱动程序的名称。
5. 实例
笔者最近设计了一块采用三星 S3C2410 ARM 处理器的电路板( ARM 处理器广泛应用于手机、 PDA 等嵌入式系统),板上包含四个用户可编程的发光二极管( LED ),这些 LED 连接在 ARM 处理器的可编程 I/O 口( GPIO )上。下图给出了 ARM 中央处理器与 LED 的连接原理:
< coordsize="21600,21600" path="m@4@5l@4@11@9@11@9@5xe" filled="f" stroked="f" style="margin: 0px; padding: 0px; max-width: 100%;"> 
 
我们在 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 的驱动实例。


 本文转自 21cnbao 51CTO博客,原文链接:http://blog.51cto.com/21cnbao/120038,如需转载请自行联系原作者




相关文章
|
3月前
|
Shell Linux
Linux shell编程学习笔记30:打造彩色的选项菜单
Linux shell编程学习笔记30:打造彩色的选项菜单
|
1月前
|
运维 监控 Shell
深入理解Linux系统下的Shell脚本编程
【10月更文挑战第24天】本文将深入浅出地介绍Linux系统中Shell脚本的基础知识和实用技巧,帮助读者从零开始学习编写Shell脚本。通过本文的学习,你将能够掌握Shell脚本的基本语法、变量使用、流程控制以及函数定义等核心概念,并学会如何将这些知识应用于实际问题解决中。文章还将展示几个实用的Shell脚本例子,以加深对知识点的理解和应用。无论你是运维人员还是软件开发者,这篇文章都将为你提供强大的Linux自动化工具。
|
3月前
|
Shell Linux
Linux shell编程学习笔记82:w命令——一览无余
Linux shell编程学习笔记82:w命令——一览无余
|
3月前
|
Linux Shell
Linux系统编程:掌握popen函数的使用
记得在使用完 `popen`打开的流后,总是使用 `pclose`来正确关闭它,并回收资源。这种做法符合良好的编程习惯,有助于保持程序的健壮性和稳定性。
118 6
|
3月前
|
监控 Linux Shell
30 个实用的 Linux 命令贴与技巧,提升你的效率(附实战案例)
本文介绍了30个实用的Linux命令及其应用场景,帮助你提升命令行操作效率。涵盖返回目录、重新执行命令、查看磁盘使用情况、查找文件、进程管理、网络状态监控、定时任务设置等功能,适合各水平的Linux用户学习和参考。
|
3月前
|
Linux Shell
Linux系统编程:掌握popen函数的使用
记得在使用完 `popen`打开的流后,总是使用 `pclose`来正确关闭它,并回收资源。这种做法符合良好的编程习惯,有助于保持程序的健壮性和稳定性。
148 3
|
3月前
|
Linux 程序员 编译器
Linux内核驱动程序接口 【ChatGPT】
Linux内核驱动程序接口 【ChatGPT】
|
3月前
|
Shell Linux Python
python执行linux系统命令的几种方法(python3经典编程案例)
文章介绍了多种使用Python执行Linux系统命令的方法,包括使用os模块的不同函数以及subprocess模块来调用shell命令并处理其输出。
53 0
|
4月前
|
项目管理 敏捷开发 开发框架
敏捷与瀑布的对决:解析Xamarin项目管理中如何运用敏捷方法提升开发效率并应对市场变化
【8月更文挑战第31天】在数字化时代,项目管理对软件开发至关重要,尤其是在跨平台框架 Xamarin 中。本文《Xamarin 项目管理:敏捷方法的应用》通过对比传统瀑布方法与敏捷方法,揭示敏捷在 Xamarin 项目中的优势。瀑布方法按线性顺序推进,适用于需求固定的小型项目;而敏捷方法如 Scrum 则强调迭代和增量开发,更适合需求多变、竞争激烈的环境。通过详细分析两种方法在 Xamarin 项目中的实际应用,本文展示了敏捷方法如何提高灵活性、适应性和开发效率,使其成为 Xamarin 项目成功的利器。
53 1
|
4月前
|
网络协议 Linux Shell
探索Linux操作系统:从基础到高级编程
【8月更文挑战第31天】本文旨在为读者提供一条清晰的路径,从Linux操作系统的基础知识出发,逐步深入到高级编程技巧。我们将一起揭开Linux神秘的面纱,了解其内部工作原理,并通过实际代码示例加深理解。无论你是初学者还是有一定经验的开发者,这篇文章都将为你带来新的视角和技能提升。