2009年10月29日 星期四

如何撰寫一個serial port的驅動程式

簡介

RS232是kernel中常用到的一種週邊元件,在以前主要是拿來連接終端設備的通訊介面,而今日則是常被拿來做控制或者和其它設備通訊的介面,因為其硬體簡單及成本低所以會被很多設備做為最基本的通訊介面,因為其發展有很長一段時間,所以在Linux中有為它特別規劃一組軟體堆疊,主要分為實體控制層以及上層的終端控制層,其架構如下:


















初始化
所以通常我們要寫的是實體層的驅動程式,在driver的進入點,若是PCI介面,則先註冊一個PCI的call back function,若是local bus則是可以註冊一個platform device call back function,當系統進入你所註冊的call back function,必須對你所使用的硬體介面位址做一個remapping的動作,就是將實體位址轉換成虛擬位址,接著後面的所有程式碼都要使用這個虛擬位址,這時要開始初始化你的硬體及driver,首先建立一個struct tty_driver的資料結構,其宣告如下:

struct tty_driver {
int magic; /* magic number for this structure */
struct cdev cdev;
struct module *owner;
const char *driver_name;
const char *name;
int name_base; /* offset of printed name */
int major; /* major device number */
int minor_start; /* start of minor device number */
int minor_num; /* number of *possible* devices */
int num; /* number of devices allocated */
short type; /* type of tty driver */
short subtype; /* subtype of tty driver */
struct ktermios init_termios; /* Initial termios */
int flags; /* tty driver flags */
int refcount; /* for loadable tty drivers */
struct proc_dir_entry *proc_entry; /* /proc fs entry */
struct tty_driver *other; /* only used for the PTY driver */

/*
* Pointer to the tty data structures
*/
struct tty_struct **ttys;
struct ktermios **termios;
struct ktermios **termios_locked;
void *driver_state; /* only used for the PTY driver */

/*
* Interface routines from the upper tty layer to the tty
* driver. Will be replaced with struct tty_operations.
*/
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
void (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty, struct file * file,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty, struct file * file,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
void (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*read_proc)(char *page, char **start, off_t off,
int count, int *eof, void *data);
int (*write_proc)(struct file *file, const char __user *buffer,
unsigned long count, void *data);
int (*tiocmget)(struct tty_struct *tty, struct file *file);
int (*tiocmset)(struct tty_struct *tty, struct file *file,
unsigned int set, unsigned int clear);

struct list_head tty_drivers;
};

在這個資料結構中其中分為幾個部分,第一個部分是所以謂的callback function的註冊,在這些個callback function中你必須金對你有實現的部分給予註冊,若沒有就可以給予不註冊,但相對的你的驅動程式所支援的功能會較少。另一個部分則是驅動程式本身資訊的資料,如major number、starting minor number、tty device name等等,以下是一個例子:

struct tty_driver MySerialDriver;

#define MAX_PORTS 4

struct ktermios *my_termios[MAX_PORTS];

struct ktermios *my_termios_locked[MAX_PORTS];

memset(&MySerialDriver, 0 sizeof(MySerialDriver));

MySerialDriver.name = "ttyN";

MySerialDriver.major = 40;

MySerialDriver.minor_start = 0;

MySerialDriver.minor_num = MAX_PORTS;

MySerialDriver.num = MAX_PORTS;

MySerialDriver.magic = TTY_DRIVER_MAGIC;

MySerialDriver.type = TTY_DRIVER_TYPE_SERIAL;

MySerialDriver.subtype = SERAL_TYPE_NORMAL;

MySerialDriver.init_termios = tty_std_termios;

MySerialDriver.init_termios.c_cflag = B9600 CS8 CREAD CLOCAL;

MySerialDriver.flags = TTY_DRIVER_REAL_RAW;

MySerialDriver.termios = my_termios;

MySerialDriver.termios_locked = my_termios_locked;

MySerialDriver.open = my_open;


以下為driver initialize的工作內容

1. allocate tty driver資料結構使用

  • struct tty_driver *mxvar_sdriver=alloc_tty_driver(MXSER_PORTS + 1);
2. 在allocate的tty driver資料結構中填入必要的資訊

  • mxvar_sdriver->owner = THIS_MODULE;
  • mxvar_sdriver->magic = TTY_DRIVER_MAGIC;
  • mxvar_sdriver->name = "ttyMI";
  • mxvar_sdriver->major = ttymajor;
  • mxvar_sdriver->minor_start = 0;
  • mxvar_sdriver->num = MXSER_PORTS + 1;
  • mxvar_sdriver->type = TTY_DRIVER_TYPE_SERIAL;
  • mxvar_sdriver->subtype = SERIAL_TYPE_NORMAL;
  • mxvar_sdriver->init_termios = tty_std_termios;
  • mxvar_sdriver->init_termios.c_cflag = B9600|CS8|CREAD|HUPCL|CLOCAL;
  • mxvar_sdriver->flags = TTY_DRIVER_REAL_RAW|TTY_DRIVER_DYNAMIC_DEV;

3. 設定tty會使用的各個call back function

  • tty_set_operations(mxvar_sdriver, &mxser_ops);
而其中的mxser_ops設定如下:
static const struct tty_operations mxser_ops = {
.open = mxser_open,
.close = mxser_close,
.write = mxser_write,
.put_char = mxser_put_char,
.flush_chars = mxser_flush_chars,
.write_room = mxser_write_room,
.chars_in_buffer = mxser_chars_in_buffer,
.flush_buffer = mxser_flush_buffer,
.ioctl = mxser_ioctl,
.throttle = mxser_throttle,
.unthrottle = mxser_unthrottle,
.set_termios = mxser_set_termios,
.stop = mxser_stop,
.start = mxser_start,
.hangup = mxser_hangup,
.break_ctl = mxser_rs_break,
.wait_until_sent = mxser_wait_until_sent,
.tiocmget = mxser_tiocmget,
.tiocmset = mxser_tiocmset,
};

4. 向tty_io註冊driver

  • retval = tty_register_driver(mxvar_sdriver);

5. 註冊 PCI client或者platform的driver, 等待其call back所設定probe程序

  • retval = pci_register_driver(&mxser_driver);
PCI client driver的資料結構如下:
static struct pci_driver mxser_driver = {
.name = "mxser",
.id_table = mxser_pcibrds,
.probe = mxser_probe,
.remove = __devexit_p(mxser_remove)
};

或者
  • retval = platform_register_driver(&mxser_driver);
platform client driver的資料結構如下:
static struct platform_driver moxaarm11_gmac_driver = {
.driver.name = DRV_NAME,
.probe = moxaarm11_gmac_probe,
.remove = moxaarm11_gmac_remove,

};

6. 這時HAL層(PCI or platform)若有找到該元件, 將會呼叫設定的probe call back function


probe call back function內做的事情

1. 取得所分配到的resource, 包含I/O及IRQ

2. 向kernel註冊所需的resource, 若是使用memory的方式來存取, 記得需要將取得的physical address用ioremap將其轉換為virtual address

3. 註冊IRQ service routine (ISR)

4. 針對每一個實體的port向上層tty做註冊

  • tty_register_device(mxvar_sdriver, brd->idx + i, &pdev->dev);


remove call back function內做的事情

1. 將取得resource釋放出來

2. 將已remap過的virtual address做unremap的動作

3. 取消已註冊的ISR

4. 針對每一個已註冊的實體port向上層tty做取消的動作

  • tty_unregister_device(mxvar_sdriver, brd->idx + i);
原則上remove的動作剛好都和probe做相反的動作


各個operation call back function內做的事情
這些的call back function都會先經過tty_io的處理之後,才會再呼叫這些call back function。

open
這是當AP呼叫open()時會被呼叫的進入點,但是在呼叫你的open call back function之前,會先經過tty_io處理過,在你的open call back function中如何取得的minor number? 可以從tty_io傳給你的tty_struct structure 中的成員index取得(tty->index),你若有控制多port的serial port這時你就可以知道AP要open那一個port,另外linux是一個多工的作業系統,所以有可能多個AP會呼叫下來,也許會open同一個port,這時driver本身自已要記住該port已被open多少次,當是第一個open時需初始化該port,以及buffer的allocate,另外serial port的硬體設定是保留前一個設定,在open時不會做更動。

close
從call back的名字就可以知道,這是從AP的close()時的進入點,這個call back function應該檢查是否為最後一個close,也就之前已有多個open()之中的最後一個close(), 記得前面的open會記住被open幾次,在close則相對地減掉被open的數,在最後一次的close時,應將open中所取得的resource應全數release給kernel。

write
這是送資料的進入點, 通常我們都會先將資料放在自已的buffer, 之後再慢慢送, 而且通常會使用interrupt的方式來送, 而這個return值會是有多少bytes已存在你的buffer中

put_char
serial communication中有所謂software flow control, 它是用資料中兩個bytes XON(0x11), XOFF (0x13)來做這流量控制, 而當要送這個bytes時就會呼叫這一個call back function, 而且這兩個必須要馬上被送出, 不可放在buffer之中, 所以原則上會在這個call back function做一個flag記錄, 而在interrupt service routine中即時送出

flush_chars
這是清除本來在put_char中的資料, 若尚未送出將給予丟棄

write_room
檢查送的buffer還剩多少空間可以存放資料

char_in_buffer
已收進多少資料在driver buffer之中

ioctl
這是相對應AP的ioctl() API, 在linux的tty中已定義一些ioctl() command, 應給予支援, 你也可以自行定義一些ioctl()

set_termios
設定RS232如baud rate, data bits, stop bits等等, 將會使用struct termios來做設定資料結構

throttle
這是在做flow control用的, 在以往hardware不夠好的時候, flow control是軟體來實現, 而且由tty_io來控制, 現在硬體已比以前強了, 所以flow control部分將會由硬體來實現, 而這個將是停止送資料, 不用管是由software flow control or hardware flow control來的, 這個call back function被呼叫時, 就是停止送資料

unthrottle
這個動作也是在做flow control, 和上面的throttle相反, 則是開始送資料

stop
shutdown該UART port

start
啓動該port

hangup
這是在接modem時用的, 用在掛電話

break_ctrl
這是在送break訊號的控制

flush_buffer
這是清除在driver buffer中的資料, 將存放在buffer中尚未處理的資料全部給予丟棄

wait_until_sent
等待在buffer中尚未送出的資料全部送出, 而且需要block在這裏面

tiocmget
最得modem的訊號 CTS, DSR, DCD, RI

tiocmset
控制modem訊號 RTS, DTR

在了解各個call back function的定義及作業內容,這時寫driver的人就必須依照這樣的定義來撰寫serial port device driver. 最後最重要的是interrupt service routine, 原則上收送資料都是使用interrupt的方式來收送, 但是在送資料時, 應該是一次就將FIFO填滿, 而收的部分通常設定一個FIFO的high water就產生interrupt, 因為不可在FIFO滿的時候才產生interrupt, 這樣沒有緩衝時間來處理後面持續進來的資料, 容易造成FIFO overflow, 以上是RS232 device driver的大概說明, 其它還有許多細節的部分留待以後再說明

2 則留言:

  1. flush_chars 不是將 在輸出緩衝區的資料全數寫出到硬體 嗎?

    回覆刪除
  2. 不是,因為沒有這樣的call back function,flush通常是將buffer中的資料給予放棄

    回覆刪除