linux驅動基礎系列--Linux 串口、usb轉串口驅動分析


轉載於: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的驅動,然后才是硬件。
更准確的圖我認為如下圖所示:

更詳細的如下圖所示:

下面摘抄網上的,主要簡單介紹了上圖,寫的比較簡明、清晰

  1. tty線程規程
    以特殊的方式格式化從一個用戶或者硬件收到的數據,這種格式化常常采用一個協議轉換的形式,如虛擬終端、PPP、Bluetooth、Ir等。
  2. tty設備發送數據流程
    tty核心從一個用戶獲取將要發送給一個tty設備的數據,tty核心將數據傳遞給tty線路規程驅動,接着數據被傳遞到tty驅動,tty驅動將數據轉換為可以發送的硬件格式。
  3. 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 };

這其實是起到一個橋接作用。后面再分析這點

  1. 將該驅動對象加入到全局的鏈表。這一步就是為了上面說的橋接

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驅動的write
c = 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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM