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


免責聲明!

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



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