11、UART&TTY驅動

  Linux系統中UART驅動和TTY驅動二者有着緊密的關係,它們不像I2C和SPI驅動是單獨一個模塊,分析時應當將它們當作一個總體來分析。UART驅動部分依賴於硬件平臺,而TTY驅動和具體的平臺無關。本文的分析內容基於IMX6DL硬件平臺和Kernel 3.0.35版本,雖然UART部分依賴於平臺,可是無論是哪一個硬件平臺,驅動的思路都是一致的,下面分模塊來分別介紹。數組

1、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的數據類型及內容以下:3d

 

 

當調用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結構體以下:orm

 

 

 

該結構體中的函數都是和具體的硬件平臺相關,串口的數據接收、註冊中斷接收函數、使用DMA接收數據等操做都是在上面的函數中完成,這些函數由NXP官方提供,是和底層硬件最接近的函數。blog

跟其餘的驅動同樣,當打開串口設備時,uart_open函數獲得調用,在tty_open函數中調用了uart_startup函數來啓動串口,以下:隊列

 

 

在uart_startup函數中經過uport->ops->startup(uport);間接調用到了imx_startup函數,由於咱們在前面已經經過sport->port.ops = &imx_pops將相關硬件平臺的串口操做函數賦值給了抽象的串口端口操做函數,因此到這裏咱們轉去分析imx_startup看看裏面作了什麼操做。ip

在imx_startup中經過調用request_irq(sport->rxirq, imx_rxint, 0, DRIVER_NAME, sport)註冊了串口中斷接收函數imx_rxint,串口中斷髮送函數同理,同時若是板級文件中設置啓用了DMA,還初始化了用於DMA數據處理相關的工做隊列,以下圖:資源

 

 

咱們並未配置使用DMA,因此只分析中斷接收函數imx_rxint。Imx_rxint函數以下:get

 

 

 

 

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的框架。

1、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得到了數據。

相關文章
相關標籤/搜索