編寫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定義,該結構有很多本驅動不實現的抽象介面函數。
struct file_operations {<br />loff_t (*llseek) (struct file *, loff_t, int);<br />ssize_t (*read) (struct file *, char *, size_t, loff_t *);<br />ssize_t (*write) (struct file *, const char *, size_t, loff_t *);<br />int (*readdir) (struct file *, void *, filldir_t);<br />unsigned int (*poll) (struct file *, struct poll_table_struct *);<br />int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);<br />int (*mmap) (struct file *, struct vm_area_struct *);<br />int (*open) (struct inode *, struct file *);<br />int (*flush) (struct file *);<br />int (*release) (struct inode *, struct file *);<br />int (*fsync) (struct file *, struct dentry *);<br />int (*fasync) (int, struct file *, int);<br />int (*check_media_change) (kdev_t dev);<br />int (*revalidate) (kdev_t dev);<br />int (*lock) (struct file *, int, struct file_lock *);<br />};
通過實現這個結構的抽象介面,告知核心[具體實現函數代碼](open, release, and ioctl)的位置。
parll.c-->file_operations parlport_fops
struct file_operations parlport_fops = {<br />.open = parlport_open,<br />.ioctl = parlport_ioctl,<br />.release = parlport_close };<br />
接著分別實現open() 和close(),本實驗這兩個函數沒有實質的工作:
parll.c-->parlport_open()
static int parlport_open(struct inode *ino, struct file *filp)<br />{<br />printk("/n parlport open function");<br />return 0;<br />}<br />static int parlport_close(struct inode *ino, struct file *filp)<br />{<br />printk("/n parlport close function");<br />return 0;<br />}
實現 ioctl()函數。注意下面的聲明代碼必須放在源碼檔案parll.c的前面:
parll.c-->parlport_ioctl()
#define MODULE_NAME "parll"<br />static int base = 0x378;<br />static int parlport_ioctl(struct inode *ino, struct file *filp, unsigned int ioctl_cmd, unsigned long parm)<br />{<br />printk("/n parlport ioctl function");<br />if(_IOC_TYPE(ioctl_cmd) != IOCTL_TYPE)<br />{<br />printk("/n%s wrong ioctl type",MODULE_NAME);<br />return -1;<br />}<br />switch(ioctl_cmd)<br />{<br />case DATA_OUT:<br />printk("/n%s ioctl data out=%x",MODULE_NAME,(unsigned int)parm);<br />outb(parm & 0xff, base+0);<br />return (parm & 0xff);<br />case GET_STATUS:<br />parm = inb(base+1);<br />printk("/n%s ioctl get status=%x",MODULE_NAME,(unsigned int)parm);<br />return parm;<br />case CTRL_OUT:<br />printk("/n%s ioctl ctrl out=%x",MODULE_NAME,(unsigned int)parm);<br />outb(parm && 0xff, base+2);<br />return 0;<br />} //end switch<br />return 0;<br />} //end 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()
static int parll_init(void)<br />{<br />int retval;<br />retval= register_chrdev(Major, MODULE_NAME, &parlport_fops);<br />if(retval < 0)<br />{<br />printk("/n%s: can't register",MODULE_NAME);<br />return retval;<br />}<br />else<br />{<br />Major=retval;<br />printk("/n%s:registered, Major=%d",MODULE_NAME,Major);<br />if(request_region(base,3,MODULE_NAME))<br />printk("/n%s:I/O region busy.",MODULE_NAME);<br />}<br />return 0;<br />}
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-->parll_cleanup( )
static void parll_cleanup( void )<br />{<br />printk("/n%s:cleanup ",MODULE_NAME);<br />release_region(base,3);<br />unregister_chrdev(Major,MODULE_NAME);<br />}
Finally, we include the required init and cleanup entry points.
parll.c
module_init(parll_init);<br />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
//application to use parallel port driver<br />#include <fcntl.h><br />#include <linux/ioctl.h><br />#include "parll.h" //把IOCTL操作命令的宏定義包進來<br />main()<br />{<br />int fptr;<br />int i,retval,parm =0;<br />printf("/nopening driver now");<br />if((fptr = open("/dev/parll",O_WRONLY))<0) //開啟裝置檔案並取得引用<br />{<br />printf("/nopen failed, returned=%d",fptr);<br />exit(1);<br />}<br />for(i=0;i<0xff;i++)<br />{<br />system("sleep .2"); //減慢迴圈執行速度<br />retval=ioctl(fptr,DATA_OUT,parm); //把參數的八位最低有效位(the least significant bits)寫入並口<br />retval=ioctl(fptr,GET_STATUS,parm); //讀取狀態資訊<br />//分解各狀態位;注意忙碌位(Busy*)是低位有效。<br />if(!(retval & 0x80))<br />printf("/nBusy signal count=%x",parm);<br />if(retval & 0x40)<br />printf("/nAck signal count=%x",parm);<br />//Uncomment these as you improve on the design.<br />// if(retval & 0x20)<br />// printf("/nPaper end signal count=%x",parm);<br />// if(retval & 0x10)<br />// printf("/nSelect signal count=%x",parm);<br />// if(retval & 0x08)<br />// printf("/nError signal count=%x",parm);<br />parm++;<br />}<br />//Close our module.<br />close(fptr);<br />}
如果把串連頭改裝成如下的樣子,那麼忙碌位(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.