The Linux Kernel Primer. A Top-Down Approach for x86 and PowerPC Architectures
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.
[*] 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.
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
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:
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:
|
Категории