编写Linux并行接口字符设备驱动 – Kemin

编写Linux并行接口字符设备驱动译自:《The Linux® Kernel Primer: A Top-Down Approach for x86 and PowerPC Architectures》By Claudia Salzberg Rodriguez, Gordon Fischer, Steven Smolski 刘建文略译(http://blog.csdn.net/keminlau)

KEY:Linux 驱动程序 并口

INTRO

本实验项目是为一个只有很简单功能的并行端口控制器( parallel port controller)写Linux驱动。现在的PC并口控制器一般是集成在南桥芯片的超级IO中。本实验驱动是对编写字符类型设备驱动一很好的示范。实验代码不太实用,但你可以自己动手改善它。因为我是在设备寄存器一级写代码,所以这些代码也适用于PowerPC平台。

我们并口驱动模块(没有特指的话一般指可动态加载模块)实现了标准的open()、 close()和最重要的ioctl()系统调用函数,展示了设备驱动结构和工作原理。我们没有现实read()或write(),因为本实验适合用ioctl()替代之。

我们开始先大致描述一下如何与并口通信,然后再分析最终的字符驱动模块的结构。我们使用ioctl()接口分别单独访问设备控制器的各个寄存器,最后创建一个应用程序来演示调用驱动程序。

Parallel Port Hardware

本项目使用X86作为硬件环境,不过最终驱动模块很容易被移植到PowerPC。现在并口在嵌入式 PowerPC 平台虽然还很常见,但在桌面PC已经越来越少见了,比如在第四第五代PC。

与并口通信时,C代码我们分别使用函数inb()和outb()。我也可以使用独立于具体体系(如x86和PPC)的函数readb()writeb(),这是两个在io.h的宏定义。

X86系统的并口一般集成在系统板上的Superio设备上,或者也可以以独立的扩展卡(PCI)方式添加到主机。那么底层配置方面,并口使用了什么地址与主机联系呢?又有没有使用中断信号呢?你可以进入BIOS设备查看并口占用了哪些系统I/O地址空间。一般X86系统里,并口可能使用三个基址:0x278, 0x378, 0x3bc,使用IRQ7。并口有三个8位的寄存器(一个占用一个I/O空间地址,所以并口只占三个I/O地址空间),比如以下使用0x378作基址的情况:

[*] 低电平有效(Active low)

KEMIN:都说并行端口只是一个端口,不是设备控制器,怎么还会有寄存器的概念?那并口所连接的设备控制寄存器的角色是什么?

The data register contains the 8 bits to write out to the pins on the connector.The status register contains the input signals from the connector.The control register sends specific control signals to the connector.

并口的连接头是25-pin D-shell (DB-25),以下是连接头的针脚与信号的映射表:

注意:并口对静电和电流是敏感的。请不要用你们的板载并口作实验,除非你对硬件很熟悉或不担心烧掉主板。建议使用独立的并口卡作实验。

改装连接头(看逻辑图)

For input operations, we will jumper D7 (pin 9) to Acknowledge (pin 10) and D6 (pin 8) to Busy (pin 11) with 470 ohm resistors. To monitor output, we drive LEDs with data pins D0 through D4 by using a 470 ohm current limiting resistor. We can do this by using an old printer cable or a 25-pin male D-Shell connector from a local electronics store.

datasheet

一位优秀的寄存器级别的程序员(KEMIN:其实就是汇编程序员)总是非常熟悉他手上硬件的细节。包括很迅速地查找出某一个并口设备的 datasheet(KEMIN:注意是设备[控制器]的 datasheet,不是并口的datasheet,并口本身不是设备)。datasheet包含了诸如控制器各寄存器功能及设备电流上限等一些设备操作信息。程序员在操作前必须仔细阅读设备[控制器]的datasheet。

Parallel Port Software

本项目实验的驱动源码主要是三个:parll.c、parll.h和Make文件。

1. Setting Up the File Operations (fops) 创建文件操作数据结构(fops)

上面已经说了,驱动模块实现了标准的open()、 close()和ioctl()函数,此外还需要模块初始化代码和清理工作代码。

我们的第一步工作是创建文件操作结构(file operations structure)。这个结构在 /linux/fs.h定义,该结构有很多本驱动不实现的抽象接口函数。

通过实现这个结构的抽象接口,告知内核[具体实现函数代码](open, release, and ioctl)的位置。 parll.c–>file_operations parlport_fops

接着分别实现open() 和close(),本实验这两个函数没有实质的工作:parll.c–>parlport_open()

实现 ioctl()函数。注意下面的声明代码必须放在源码文件parll.c的前面:

parll.c–>parlport_ioctl()

ioctl()函数设计的目的是给用户实现自定义的I/O操作(KEMIN:也就是标准I/O操作(像读或写)不能满足的操作)。本项目简单的演示了通过并口实现三个I/O命令(command)访问控制器寄存器数据:

* The DATA_OUT command sends a value to the data register,

* the GET_STATUS command reads from the status register,

* the CTRL_OUT command is available to set the control signals to the port.

虽然根据[美化原则](better methodology),读写设备操作的特定细节应该分别隐藏于read() 和write()两个接口函数内,但是本实验主要目的是演示I/O,而不是数据封装,所以选择使用ioctl。

代码中使用的三个I/O宏命令在头文件 parll.h中定义。parlport_ioctl() 的开始处有对输入命令的合法检测的逻辑。由于应用程序会引用同一个头文件,所以这些命令的定义是一致的。

2. Setting Up the Module Initialization Routine 创建模块初始化函数

初始化代码负责把驱动模块关联入操作系统,复杂一些驱动类型(比如USB)可能需要一些必要的数据结构会在这个时候被初始化。因为并口驱动不需复杂的数据结构,所以以下只是简单的地把驱动模块注册入操作系统:

parll.c–>parll_init()

The register_chrdev() function takes in the requested major number (discussed in div 5.2 and later in Chapter 10; if 0, the kernel assigns one to the module). Recall that the major number is kept in the inode structure, which is pointed to by the dentry structure, which is pointed to by a file struct. The second parameter is the name of the device as it will appear in /proc/devices. The third parameter is

the file operations structure that was just shown.

Upon successfully registering, our init routine calls request_region() with the base address of the parallel port and the length (in bytes) of the range of registers we are interested in.

The init_module() function returns a negative number upon failure.

3. Setting Up the Module Cleanup Routine 创建模块清理函数

The cleanup_module() function is responsible for unregistering the module and releasing the I/O range that we requested earlier:

parll.c–>parll_cleanup( )

Finally, we include the required init and cleanup entry points. parll.c

4. Inserting the Module 安装驱动模块

We can now insert our module into the kernel, as in the previous projects, by using

Looking at /var/log/messages shows us our init() routine output as before, but make specific note of the major number returned.

In previous projects, we simply inserted and removed our module from the kernel. We now need to associate our module with the filesystem with the mknod command. From the command line, enter the following:

The parameters:

*c. Create a character special file (as opposed to block)

*/dev/parll. The path to our device (for the open call)

*XXX. The major number returned at init time (from /var/log/messages)

*0. The minor number of our device (not used in this example)

For example, if you saw a major number of 254 in /var/log/messages, the command would look like this:

5. Application Code 应用程序代码

Here, we created a simple application that opens our module and starts a binary count on the D0 through D7 output pins.

Compile this code with gcc app.c. The executable output defaults to a.out: app.c

如果把连接头改装成如下的样子,那么忙碌位(busy)和应答位(ack)也可高位有效。用户代码可以分别读取两最高有效数据位代表它们。

Figure 5.5. Built Connector

本实验项目只是演示了编写一个字符设备驱动需要的主要元素。不过有了这个演示例子,你完全可以编写自己的字符设备驱动程序了。如果要为这个驱动添加中断处理,则必须在 init_module()内调用request_irq()并传递所需IRQ和中断处理函数的函数名。

以下是对本实验驱动程序改进的一些建议:

Here are some suggested additions to the driver:

*Make parallel port module service-timer interrupts to poll input. How can we multiplex 8 bits of I/O into 16, 32, 64? What is sacrificed?Send a character out the serial port from the write routine within the module.Add an interrupt routine by using the ack signal.却只能这样。只有对爱的人,我们才会斤斤计较,锱铢必较。

编写Linux并行接口字符设备驱动 –  Kemin

相关文章:

你感兴趣的文章:

标签云: