十一、UART&TTY驅動
Linux系統中UART驅動和TTY驅動兩者有著緊密的關係,它們不像I2C和SPI驅動是單獨一個模組,分析時應當將它們看成一個整體來分析。UART驅動部分依賴於硬體平臺,而TTY驅動和具體的平臺無關。本文的分析內容基於IMX6DL硬體平臺和Kernel 3.0.35版本,雖然UART部分依賴於平臺,但是不管是哪個硬體平臺,驅動的思路都是一致的,下面分模組來分別介紹。
一、UART驅動
UART驅動主要涉及的驅動檔案是imx.c、serial_core.c兩個檔案。首先我們找到驅動的入口函式module_init(imx_serial_init),在函式imx_serial_init中呼叫uart_register_driver向核心註冊了一個驅動,在該函式中除了做常規的初始化驅動之外,有兩個關鍵點的函式呼叫需要我們注意一下,如下圖:
先是呼叫tty_set_operations將uart_ops這一個tty裝置的操作函式集設定到了tty驅動中,同時呼叫tty_register_driver函式向核心註冊了tty驅動,其中uart_ops的資料型別及內容如下:
當呼叫tty_open函式時就會呼叫這裡的uart_open,具體是怎麼呼叫的,我們後面會分析到。imx_serial_init函式中還呼叫platform_driver_register向核心註冊了一個平臺裝置,所以UART驅動即是平臺裝置又是字元裝置。當驅動和裝置匹配時會呼叫serial_imx_probe函式,在該函式中除了做具體平臺相關的串列埠埠設定,比如呼叫platform_get_resource獲取中斷資源,賦值sport->timer.functioni = mx_timeout設定定時器之外,還有一個關鍵的操作就是sport->port.ops = &imx_pops,賦值了跟具體硬體平臺的底層操作函式,當中的imx_pops結構體如下:
該結構體中的函式都是和具體的硬體平臺相關,串列埠的資料接收、註冊中斷接收函式、使用DMA接收資料等操作都是在上面的函式中完成,這些函式由NXP官方提供,是和底層硬體最接近的函式。
跟其他的驅動一樣,當開啟串列埠裝置時,uart_open函式得到呼叫,在tty_open函式中呼叫了uart_startup函式來啟動串列埠,如下:
在uart_startup函式中通過uport->ops->startup(uport);間接呼叫到了imx_startup函式,因為我們在前面已經通過sport->port.ops = &imx_pops將相關硬體平臺的串列埠操作函式賦值給了抽象的串列埠埠操作函式,所以到這裡我們轉去分析imx_startup看看裡面做了什麼操作。
在imx_startup中通過呼叫request_irq(sport->rxirq, imx_rxint, 0, DRIVER_NAME, sport)註冊了串列埠中斷接收函式imx_rxint,串列埠中斷髮送函式同理,同時如果板級檔案中設定啟用了DMA,還初始化了用於DMA資料處理相關的工作佇列,如下圖:
我們並未配置使用DMA,所以只分析中斷接收函式imx_rxint。Imx_rxint函式如下:
imx_rxint函式在迴圈中讀取資料暫存器的值,並在函式的末尾呼叫了兩個很關鍵的函式,分別是tty_insert_flip_char(tty, rx, flg)和tty_flip_buffer_push(tty),其中tty_insert_flip_char函式的作用是將接收到的字元放入tty資料塊中,如下圖:
而tty_flip_buffer_push(tty)則是將tty資料塊的資料推到線路規程當中,線路規程相關的知識我們後面會講到,這個函式的作用就類似於通知tty去線路規程獲取從串列埠過來的資料,函式內容如下:
其中有個關鍵的操作就是呼叫了工作佇列,具體這個工作佇列是在何時被註冊或者初始化,我們後面講tty時候會分析到。總結以上,如果中斷函式中只調用tty_insert_flip_char函式的話,tty是沒辦法獲取串列埠資料的,還必須使用tty_flip_buffer_push函式將資料推到線路規程當中去。至此,UART到TTY這條路徑我們就分析完了,接下來分析TTY的框架。
一、TTY驅動
TTY驅動不依賴具體的硬體平臺,主要涉及的檔案是tty_io.c、tty_ldisc.c。TTY驅動框架中包含一個叫線路規程的核心模組,TTY驅動不能直接從UART獲取資料,所有的資料都必須從ldisc(線路規程獲取)。首先我們來看tty相關的初始化,在前面註冊UART驅動的時候,同時呼叫了tty_register_driver(normal)函式向核心註冊了一個tty驅動,在該函式中呼叫了cdev_init(&driver->cdev, &tty_fops),向裝置綁定了tty裝置的操作函式集,tty_fops的資料型別是struct file_operations,該變數如下圖:
因此當應用層開啟一個tty裝置時候會呼叫這個函式集當中的tty_open函式,接下來我們看tty_open函式裡面做了什麼操作。在tty_open函式中呼叫tty_init_dev(driver, index, 0)函式對tty裝置進行了初始化,在tty_init_dev函式中又呼叫了initialize_tty_struct(tty, driver, idx)函式對tty相關的結構體進行了初始化,如下圖所示:
其中有三個地方需要我們重點關注,第一個是tty_ldisc_init(tty),呼叫該函式完成了線路規程的初始化,在tty_ldisc_init函式裡面通過呼叫tty_ldisc_get獲得線路規程,在tty_ldisc_get函式中通過呼叫get_ldops(disc)獲得線路規程的操作函式,如圖所示:
其中tty_ldiscs是一個全域性陣列,陣列元素型別是struct tty_ldisc_ops,也就是線路規程的操作函式集,型別如下圖:
線路規程的操作函式具體是在什麼時候被賦值初始化的,我們後面會分析到。
在initialize_tty_struct函式中第二個需要我們關注的函式呼叫是tty_buffer_init(tty),,
呼叫該函式完成了tty資料塊相關的初始化,如下圖所示:
在初始化函式中還初始化了一個工作佇列,INIT_WORK(&tty->buf.work, flush_to_ldisc)。
具體這個工作佇列是在何時被呼叫呢?就是在我們前面分析imx_rxint中斷接收函式時,呼叫了tty_flip_buffer_push,在該函式中通過schedule_work(&tty->buf.work)排程了該工作佇列。至此,TTY也和UART聯絡上了。
在initialize_tty_struct函式中需要我們關注的地方是tty->ops = driver->ops語句。前面我們分析到,在串列埠註冊時候呼叫tty_set_operations函式,通過driver->ops = op將tty的操作函式賦值給了uart驅動,在這裡則是將註冊進去的函式給拿出來賦值給了tty裝置,等於是應用層操作tty裝置就是操作uart串列埠。在tty_init_dev函式中,除了初始化tty裝置之外,還呼叫tty_ldisc_setup(tty, tty->link)函式對線路規程進行了設定。在tty_ldisc_setup函式中呼叫了tty_ldisc_open函式,該函式中使用ld->ops->open(tty)打開了線路規程,但是線路規程的操作函式是在哪裡進行賦值的呢?保留這個疑問,我們接下來分析線路規程相關的初始化流程。
記得前面我們提到的一個全域性陣列tty_ldiscs嗎?這個陣列的元素型別就是線路規程的操作函式。我們在核心程式碼中進行全域性搜尋,發現在tty_register_ldisc函式中進行了設定,如下圖:
呼叫該函式的話,就會將線路規程設定到全域性陣列tty_ldiscs中,那麼tty_register_ldisc函式是在哪裡被呼叫的呢?答案是,在tty_ldisc_begin函式中被呼叫,如下圖:
而tty_ldisc_N_TTY變數就是線路規程的操作函式,變數賦值如下圖:
tty_ldisc_begin這個函式被console_init呼叫,那是誰又呼叫了console_init呢?答案是在/init/main.c檔案中,asmlinkage void __init start_kernel(void)函式呼叫了console_init。而start_kernel函式正是核心的入口函式。也就是說,在進入核心的時候,第一時間就先初始化了tty的線路規程,賦值了線路規程的相關操作函式。那線路規程的操作函式又是在哪裡被呼叫的呢?
前面我們講過,tty驅動不能直接從串列埠獲得資料,資料的來源是線路規程,那麼呼叫線路規程的讀寫函式只能是tty的操作函式,所以我們來看看之前從未分析的tty_read和tty_write函式。首先來看tty_read函式,如下圖:
果不其然,在tty_read中通過ld->ops->read呼叫了線路規程的read函式,也就是呼叫了tty_ldisc_N_TTY的ntty_read函式。我們再來看tty_write函式,如下圖:
同樣是呼叫到了線路規程的n_tty_write函式。
綜上,在進入核心的時候,先是設定了線路規程的操作函式,然後在tty驅動註冊的時候設定了tty的操作函式,並在後續開啟tty裝置時呼叫tty_open函式,在open函式中通過get_ldops(disc)獲得線路規程的操作函式。當應用層呼叫tty_read讀取資料時就呼叫了n_tty_read獲得了數