十一、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獲得了數據。