轉載於:https://www.cnblogs.com/rongpmcu/p/7662278.html
前言
主要是想對Linux 串口、usb轉串口驅動框架有一個整體的把控,因此會忽略某些細節,同時里面涉及到的一些驅動基礎,比如字符設備驅動、平台驅動等也不進行詳細說明原理。如果有任何錯誤地方,請指出,謝謝!
聲明:圖和個別段落(我做了小的修改)是直接從網上截取
整體概述
linux下的串口或者usb轉串口驅動都是依賴linux內核提供的tty核心、tty線路規划和tty驅動,所以牽涉到很多層次,之所以有這么多層次,肯定是有它們存在意義的。
舉例來說,像串口或者usb轉串口的驅動,最終可以確定的是以字符設備驅動提供給上層使用,於是tty核心層就對這部分通用的實現進行了封裝,但這不是最重要的,最重要的是tty核心層里同時實現了一種數據格式化機制,這就是tty線路規划,這樣的好處是可以分別針對不同類設備的線路規划,比如針對終端io的,比如針對網絡的ppp還有slip還有藍牙還有IrDA等,這些的實現不需要考慮底層硬件,也就是說這些串口到具體協議的轉換的實現與硬件相分離了,這就是tty核心及tty線路規划存在的目地。
那為什么會有tty驅動層呢? 也許你覺得我們的串口驅動可以直接通過tty核心提供的功能就可以實現了。 這個確實是可以,但是linux內核因為要兼容世界上存在的各種串口設備,所以針對串口額外實現了一個serial核心層,針對usb轉串口額外實現了usb-serial核心層,它們就是所謂的tty驅動層。我們的串口或者usb轉串口實現就是與tty驅動層打交道,當然串口芯片或者usb轉串口芯片有很多種,所以不同的芯片都要有對應的驅動,但是它們都是基於tty驅動層實現,這個是可以肯定的。
所以,我們要寫串口驅動,最好還是對這些層次有些了解。
整體框架圖如下:

這圖是直接摘抄網上的。其實,我認為在tty驅動層下是8250串口控制器芯片,那么應該有個8250的驅動,然后才是硬件。
更准確的圖我認為如下圖所示:

更詳細的如下圖所示:

下面摘抄網上的,主要簡單介紹了上圖,寫的比較簡明、清晰
- tty線程規程
以特殊的方式格式化從一個用戶或者硬件收到的數據,這種格式化常常采用一個協議轉換的形式,如虛擬終端、PPP、Bluetooth、Ir等。 - tty設備發送數據流程
tty核心從一個用戶獲取將要發送給一個tty設備的數據,tty核心將數據傳遞給tty線路規程驅動,接着數據被傳遞到tty驅動,tty驅動將數據轉換為可以發送的硬件格式。 - tty設備接收數據流程
從tty硬件接收到的數據向上交給tty驅動,進入tty線路規程驅動,再進入tty核心,在此被用戶獲取。盡管tty核心與tty之間的數據傳輸會經歷tty線路規程的轉換,但是tty驅動與tty核心之間也可以直接傳輸數據。
再摘抄2張網上的圖:

tty設備的數據流通圖:

tty框架分析
tty在linux下屬於字符設備驅動,tty層提供了一些數據結構和函數接口方便其他驅動注冊上來,其中包括虛擬終端、串口終端、偽終端等。Tty核心部分在tty_io.c里面實現。
第一步、內核默認的tty初始化部分
1 static int __init tty_class_init(void) 2 { 3 tty_class = class_create(THIS_MODULE, "tty"); 4 if (IS_ERR(tty_class)) 5 return PTR_ERR(tty_class); 6 tty_class->devnode = tty_devnode; 7 return 0; 8 } 9 postcore_initcall(tty_class_init); 10 上面代碼創建了tty類,方便以后創建設備節點,然后是tty_init,tty_init函數負責初始化tty層,它是由chr_dev_init調用的(fs_initcall(chr_dev_init)),也就是說它屬於字符設備一部分。 11
12 int __init tty_init(void) 13 { 14 cdev_init(&tty_cdev, &tty_fops); 15 if (cdev_add(&tty_cdev, MKDEV(TTYAUX_MAJOR, 0), 1) ||
16 register_chrdev_region(MKDEV(TTYAUX_MAJOR, 0), 1, "/dev/tty") < 0) 17 panic("Couldn't register /dev/tty driver\n"); 18 device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 0), NULL, 19 "tty"); 20
21 cdev_init(&console_cdev, &console_fops); 22 if (cdev_add(&console_cdev, MKDEV(TTYAUX_MAJOR, 1), 1) ||
23 register_chrdev_region(MKDEV(TTYAUX_MAJOR, 1), 1, "/dev/console") < 0) 24 panic("Couldn't register /dev/console driver\n"); 25 device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 1), NULL, 26 "console"); 27
28 #ifdef CONFIG_VT 29 vty_init(&console_fops); 30 #endif
31 return 0; 32 }
注:個人認為上面的if判斷寫法不是很好,雖然是正確的
這里和我們最終關心的串口驅動沒關系,但由此可以看出tty字符設備(/dev/tty)使用的主設備號是TTYAUX_MAJOR(5),次設備號為0,/dev/console使用的主設備號也是5,但次設備號為1,控制台的初始化console_init在這個函數之前會被調用(start_kernel),內核注釋如下:
* * HACK ALERT! This is early. We're enabling the console before * we've done PCI setups etc, and console_init() must be aware of * this. But we do want output early, in case something goes wrong. */ console_init();
這里不跟進去分析了。
虛擬終端、控制台部分暫時忽略不管。
第二步:使用tty層提供的功能(我們只關心串口驅動,所以是serial核心層或者usb-serial核心層使用它們),主要包含
1)tty_register_driver注冊tty驅動
相關數據結構:struct tty_driver *driver 可以通過alloc_tty_driver分配,它主要任務是
創建一個字符設備,但是這個字符設備的操作集是tty層定義的tty_fops,之所以由tty層提供,是因為它要實現線路規划部分,數據流會由它轉向線路規划部分中。
1 static const struct file_operations tty_fops = { 2 .llseek = no_llseek, 3 .read = tty_read, 4 .write = tty_write, 5 .poll = tty_poll, 6 .unlocked_ioctl = tty_ioctl, 7 .compat_ioctl = tty_compat_ioctl, 8 .open = tty_open, 9 .release = tty_release, 10 .fasync = tty_fasync, 11 };
這其實是起到一個橋接作用。后面再分析這點
- 將該驅動對象加入到全局的鏈表。這一步就是為了上面說的橋接。
2)tty_register_device注冊tty設備,只需要指定對應的驅動對象和索引號即可。它創建一個字符設備到/dev下 設備號由驅動對應的設備號base+索引。
情景分析
下面以幾個情景分析(這里只分析tty框架的處理,還沒有和具體的驅動掛鈎):
情景1:打開設備
在應用層open上文第二步中tty_register_device創建的設備,會經過vfs 最終到tty_init中注冊的tty_fops操作集里的open,也就是tty_open。它會根據你打開的是/dev/tty 還是 /dev/console 或者是你自己定義的一個設備(比如串口設備)(這個是由你tty_register_driver注冊是參數struct tty_driver *driver里面的major決定的)
這里假設打開的是自己定義的設備/dev/ttyS0,那么會通過driver = get_tty_driver(device, &index);獲取,它其實是掃描全局鏈表,這個鏈表的建立是在第二步中第2小步說明部分完成的。
如果是第一次打開,那么會創建一個新的對象用來代表這個open及以后操作的上下文,即tty_struct,通過alloc_tty_struct分配的,它里面有相應的線路規划策略tty_ldisc_init,默認初始化為tty_ldisc_get(N_TTY)。 然后調用線路規划的open。tty_struct對象同時繼承了driver的操作集tty_fops,它內部同時會分配並初始化ktermios對象tty_init_termios(tty)及在driver上登記driver->ttys[idx] = tty; 最后會調用驅動本身注冊的open。tty_struct對象會放到file的private_data,為以后操作做好准備。
情景2:從設備讀數據
在應用層read上文第二步中tty_register_device創建的設備,會經過vfs 最終到tty_init中注冊的tty_fops操作集里的read
也就是tty_read
tty設備沒有read函數,是因為大部分tty的輸入設備和輸出設備不一樣。例如我們的虛擬終端設備,它的輸入是鍵盤,輸出是顯示器。
由於這樣的原因,tty的驅動層和tty的線路規程層都有一個緩沖區。
tty驅動層的緩沖區用來保存硬件發過來的數據。在驅動程序里使用 tty_insert_flip_string函數可以實現將硬件的數據存入到驅動層的緩沖區。
其實一個緩沖區就夠了,為什么線路規程層還是有一個緩沖區呢?
那是因為tty核心無法直接讀取驅動層的緩沖區的數據。tty核心讀不到數據,用戶也就無法獲取數據。用戶的read函數只能從tty核心讀取數據。而tty核心只能從tty線路規程層的緩沖區讀取數據。
因為是層層讀寫的關系,所以tty線路規程也是需要一個緩沖區的。
在驅動程序里使用tty_flip_buffer_push()函數將tty驅動層緩沖區的數據推到tty線路規程層的緩沖區。這樣就完成了數據的流通。
因為全是緩沖區操作,所以需要兩個進程:寫數據進程和讀數據進程。
如果緩沖區內沒有數據,運行讀進程的話,tty核心就會把讀進程加入到等待隊列。
tty_read的主要流程:
從上文分析的open函數所存儲的private里面取出分配並初始化過的tty_struct對象tty = (struct tty_struct *)file->private_data;,然后它會調用屬於tty的線路規划里面的read,線路規划是通過tty_register_ldisc注冊到一個全局數組里的,對應默認的線性規划是文件tty_ldisc.c里面tty_ldisc_begin完成的,它是在console_init里被調用的,也就是內核調用tty_init之前。
1 void tty_ldisc_begin(void) 2 { 3 /* Setup the default TTY line discipline. */
4 (void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY); 5 } 6
7 struct tty_ldisc_ops tty_ldisc_N_TTY = { 8 .magic = TTY_LDISC_MAGIC, 9 .name = "n_tty", 10 .open = n_tty_open, 11 .close = n_tty_close, 12 .flush_buffer = n_tty_flush_buffer, 13 .chars_in_buffer = n_tty_chars_in_buffer, 14 .read = n_tty_read, 15 .write = n_tty_write, 16 .ioctl = n_tty_ioctl, 17 .set_termios = n_tty_set_termios, 18 .poll = n_tty_poll, 19 .receive_buf = n_tty_receive_buf, 20 .write_wakeup = n_tty_write_wakeup 21 };
因此最終調用n_tty_read,它會根據是否有數據做不同的處理,如果有數據,則直接處理后返回,如果沒有數據,那么就在等待隊列上睡眠等待。
情景2:從設備寫數據
在應用層write上文第二步中tty_register_device創建的設備,會經過vfs 最終到tty_init中注冊的tty_fops操作集里的write
也就是tty_write。Write調用要簡單很多,它調用do_tty_write,
它內部實際調用的是線路規划的n_tty_write,它當然會調用tty_struct 的write,也就是繼承自tty驅動的writec = tty->ops->write(tty, b, nr); 由驅動完成最終的操作硬件發送數據。
注意:這里描述的讀、寫是以終端io為例,如果是藍牙、或者ppp這些網絡io,read、write會通過網絡協議棧,而不是這里的tty_read tty_write。


第二步、具體驅動部分分析
1、 serial核心層(tty驅動層實現)分析
2、 串口驅動分析(8250為例)
1、 usb-serial核心層(tty驅動層實現)分析
2、 usb轉串口驅動分析(pl2303為例)
另外再上張圖
------------------未完,待續!
2014年5月
畢業那兩年在做嵌入式應用開發,主要是單片機和arm linux上的應用開發,后來又做了兩年arm linux驅動開發,15年到現在在做pc端及嵌入式端開發,包括服務器系統裁剪、底層框架實現、硬件加速等。喜歡技術分享、交流!聯系方式: 907882971@qq.com、rongpmcu@gmail.com
