Project: Building a Parallel Port Driver


This project introduces a basic parallel port controller, which demonstrates how the I/O routines previously discussed coalesce. The parallel port, usually integrated into the Superio section of a chipset, is a good example for a character device-driver skeleton. This driver, or dynamically loaded module, is not extremely useful, but you can build upon and improve it. Because we address the device at the register level, this module can be used in a PowerPC system for accessing I/O as long as the register I/O mapping is documented.

Our parallel port device driver uses the standard open(), close(), and most importantly, the ioctl() interface to illustrate the architecture and inner workings of the device driver. We won't be using the read() or write() functions in this project because the ioctl() call returns register values. (Because our device driver is a dynamically loadable module, we simply refer to it as a module.)

We begin with a brief description on how to talk to the parallel port and then proceed to investigate our basic character device-driver module operations. We use the ioctl() interface to reference the individual registers in the device, and create an application to interface with our module.

Parallel Port Hardware

Any Web search of the parallel port yields a massive amount of information. Because our goal for this section is to describe a Linux module, we touch only on the basics of this device.

For this project, we use an x86 system for the experiment. This driver skeleton is easily ported to PowerPC; it just needs to talk to another device at the I/O level. Although the parallel port exists in many embedded PowerPC implementations, it is not widely used in desktops (such as the G4 and G5).

For the actual communication with the parallel port registers, we use inb() and outb(). We could have just as easily used readb() and writeb(), which are available in the file io.h for both x86 and PPC architectures. The readb() and writeb() macros are a good choice for architecture independence because they each resolve to the low-level I/O routines that are used for x86 and PPC.

The parallel port in x86 systems is usually included as a part of the Superio device or it could be a separate (PCI) card added to the system. If you go to your BIOS setup screen, you can see where the parallel port(s) is mapped in the system I/O space. For x86 systems, the parallel port will be at hex address 0x278, 0x378, or 0x3bc using IRQ 7. This is the base address of the device. The parallel port has three 8-bit registers, starting at the base address shown in Table 5.2. For this example, we use a base address of 0x378.

Table 5.2. Parallel Port Registers

Bit

7

6

5

4

3

2

1

0

I/O Port Address

Data register (output)

D7

D6

D5

D4

D3

D2

D1

D0

0x378 (base+0)

Status register (input)

Busy[*]

ACK

Paper end

Select

Error

   

0x379 (base+1)

Control register (output)

    

Select[*]

Init

Auto feed[*]

Strobe[*]

0x37A (base+2)


[*] Active low

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.

The connector for the parallel port is a 25-pin D-shell (DB-25). Table 5.3 shows how these signals map to the specific pins of the connector.

Table 5.3. Association of Signals to Pins of the Parallel Connector

Signal Name

Pin Number

Strobe

1

D0

2

D1

3

D2

4

D3

5

D4

6

D5

7

D6

8

D7

9

Acknowledge

10

Busy

11

Paper end

12

Select in

13

Auto feed

14

Error

15

Initialize

16

Select

17

Ground

1825


CAUTION!

The parallel port can be sensitive to static electricity and overcurrent. Do not use your integrated (built in to the motherboard) parallel port unless

  • You are certain of your hardware skills.

  • You have no problem destroying your portor worse, your motherboard.

We strongly suggest that you use a parallel-port adapter card for these, and all, experiments.


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.

NOTE

A good register-level programmer should always know as much about the underlying hardware as possible. This includes finding the datasheet for your particular parallel port I/O device. In the datasheet, you can find the sink/source current limitations for your device. Many Web sites feature interface methods to the parallel port, including isolation, expanding the number of signals, and pull-up and pull-down resistors. They are a must read for any I/O controller work beyond the scope of this example.


This module addresses the parallel port by way of the outb() and inb() functions. Recall from Chapter 2, "Exploration Toolkit," that, depending on the platform compilation, these functions correctly implement the in and out instructions for x86 and the lbz and stb instructions for the memory-mapped I/O of the PowerPC. This inline code can be found in the /io.h file under the appropriate platform.

Parallel Port Software

The following discussion focuses on the pertinent driver functions for this project. The complete program listing for parll.c, along with Make and parll.h files, is included at the end of this book.

1. Setting Up the File Operations (fops)

As previously mentioned, this module uses open(), close(), and ioctl(), as well as the init and cleanup operations discussed in previous projects.

The first step is to set up our file operations structure. This structure defined in /linux/fs.h lists the possible functions we can choose to implement in our module. We do not have to itemize each operationonly the ones we want. A Web search of C99 and linux module furnishes more information on this methodology. By using this structure, we inform the kernel of the location of our implementation (or entry points) of open, release, and ioctl.

 ------------------------------------------------------------------------- parll.c struct file_operations parlport_fops = {       .open =   parlport_open,      .ioctl =  parlport_ioctl,      .release =  parlport_close }; ------------------------------------------------------------------------- 

Next, we create the functions open() and close(). These are essentially dummy functions used to flag when we have opened and closed:

 ------------------------------------------------------------------------- parll.c static int parlport_open(struct inode *ino, struct file *filp) {   printk("\n parlport open function");   return 0; } static int parlport_close(struct inode *ino, struct file *filp) {   printk("\n parlport close function");   return 0; } ------------------------------------------------------------------------- 

Create the ioctl() function. Note the following declarations were made at the beginning of parll.c:

 ------------------------------------------------------------------------- #define MODULE_NAME  "parll" static int base = 0x378; parll.c static int parlport_ioctl(struct inode *ino, struct file *filp,      unsigned int ioctl_cmd, unsigned long parm) {   printk("\n parlport ioctl function");   if(_IOC_TYPE(ioctl_cmd) != IOCTL_TYPE)   {    printk("\n%s wrong ioctl type",MODULE_NAME);    return -1;   }   switch(ioctl_cmd)   {    case DATA_OUT:     printk("\n%s ioctl data out=%x",MODULE_NAME,(unsigned int)parm);     outb(parm & 0xff, base+0);     return (parm & 0xff);    case GET_STATUS:     parm = inb(base+1);     printk("\n%s ioctl get status=%x",MODULE_NAME,(unsigned int)parm);     return parm;    case CTRL_OUT:     printk("\n%s ioctl ctrl out=%x",MODULE_NAME,(unsigned int)parm);     outb(parm && 0xff, base+2);     return 0;   }  //end switch   return 0; } //end ioctl ------------------------------------------------------------------------- 

The ioctl() function is made available to handle any user-defined command. In our module, we surface the three registers associated with the parallel port to the user. The DATA_OUT command sends a value to the data register, the GET_STATUS command reads from the status register, and finally, the CTRL_OUT command is available to set the control signals to the port. Although a better methodology would be to hide the device specifics behind the read() and write() routines, this module is mainly for experimentation with I/O, not data encapsulation.

The three commands just used are defined in the header file parll.h. They are created by using the IOCTL helper routines for type checking. Rather than using an integer to represent an IOCTL function, we use the IOCTL type checking macro IO(type,number), where the type is defined as p (for parallel port) and number is the actual IOCTL number used in the case statement. At the beginning of parlport_ioctl(), we check the type, which should be p. Because the application code uses the same header file as the driver, the interface will be consistent.

2. Setting Up the Module Initialization Routine

The initialization module is used to associate the module with the operating system. It can also be used for early initialization of any data structures if desired. Since the parallel port driver requires no complex data structures, we simply register the module.

 ------------------------------------------------------------------------- parll.c static int parll_init(void) {   int retval;   retval= register_chrdev(Major, MODULE_NAME, &parlport_fops);   if(retval < 0)   {    printk("\n%s: can't register",MODULE_NAME);    return retval;   }   else   {    Major=retval;    printk("\n%s:registered, Major=%d",MODULE_NAME,Major);    if(request_region(base,3,MODULE_NAME))     printk("\n%s:I/O region busy.",MODULE_NAME);   }   return 0; } ------------------------------------------------------------------------- 

The init_module() function is responsible for registering the module with the kernel. The register_chrdev() function takes in the requested major number (discussed in Section 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   static void parll_cleanup( void ) {    printk("\n%s:cleanup ",MODULE_NAME);    release_region(base,3);    unregister_chrdev(Major,MODULE_NAME); } ------------------------------------------------------------------------- 

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

 ----------------------------------------------------------------------- parll.c module_init(parll_init); module_exit(parll_cleanup); ----------------------------------------------------------------------- 

4. Inserting the Module

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

 Lkp:~# insmod parll.ko 

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:

 Lkp:~# mknod /dev/parll c <XXX> 0 

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:

   Lkp:~# mknod /dev/parll c 254 0 

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 000  //application to use parallel port driver #include <fcntl.h> #include <linux/ioctl.h> 004  #include "parll.h" main() {   int fptr;   int i,retval,parm =0;   printf("\nopening driver now"); 012   if((fptr = open("/dev/parll",O_WRONLY))<0)   {    printf("\nopen failed, returned=%d",fptr);    exit(1);   } 018   for(i=0;i<0xff;i++)   { 020    system("sleep .2"); 021    retval=ioctl(fptr,DATA_OUT,parm); 022    retval=ioctl(fptr,GET_STATUS,parm); 024    if(!(retval & 0x80))     printf("\nBusy signal count=%x",parm);    if(retval & 0x40) 027     printf("\nAck signal count=%x",parm); 028  //   if(retval & 0x20) //    printf("\nPaper end signal count=%x",parm); //   if(retval & 0x10) //    printf("\nSelect signal count=%x",parm); //   if(retval & 0x08)  033  //    printf("\nError signal count=%x",parm);    parm++;   } 038   close(fptr); } ------------------------------------------------------------------------- 

Line 4

The header file common to both the application and the driver contains the new IOCTL helper macros for type checking.

Line 12

Open the driver to get a file handle for our module.

Line 18

Enter the loop.

Line 20

Slow down the loop so we can watch the lights/count.

Line 21

Using the file pointer, send a DATA_OUT command to the module, which in turn uses outb() to write the least significant 8 bits of the parameter to the data port.

Line 22

Read the status byte by way of the ioctl with a GET_STATUS command. This uses inb() and returns the value.

Lines 2427

Watch for our particular bits of interest. Note that Busy* is an active low signal, so when the I/O is off, we read this as true.

Lines 2833

Uncomment these as you improve on the design.

Line 38

Close our module.

If you have built the connector as outlined in Figure 5.5, the busy and ack signals come on when the two most significant bits of the count are on. The application code reads these bits and outputs accordingly.

Figure 5.5. Built Connector


We just outlined the major elements for a character device driver. By knowing these functions, it is easier to trace through working code or create your own driver. Adding an interrupt handler to this module involves a call to request_irq() and passing in the desired IRQ and the name of the handler. This would be included in the init_module().

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.




The Linux Kernel Primer. A Top-Down Approach for x86 and PowerPC Architectures
The Linux Kernel Primer. A Top-Down Approach for x86 and PowerPC Architectures
ISBN: 131181637
EAN: N/A
Year: 2005
Pages: 134

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net