linux UART串口驅動開發文檔

admin @ 2014-03-25 , reply:0

w83697/w83977 super I/O串口驅動開發
內容簡介: 介紹了Linux下的串口驅動的設計層次及介面, 並指出串口與TTY終端之間的關聯層次(串口可作TTY終端使用), 以及Linux下的中斷處理機制/中斷共享機制, 還有串口緩衝機制當中涉及的軟中斷機制; 其中有關w83697/w83977 IC方面的知識, 具體參考相關手冊, 對串口的配置寄存器有詳細介紹, 本文不再進行說明.
目錄索引:
一. Linux的串口介面及層次.
二. Linux的中斷機制及中斷共享機制.
三. Linux的軟中斷機制.
四. TTY與串口的具體關聯.

一. Linux的串口介面及層次.
    串口是使用已經非常廣的設備了, 因此在linux下面的支持已經很完善了, 具有統一的編程介面, 驅動開發者所要完整的工作就是針對不同的串口IC來做完成相應的配置宏, 這此配置宏包括讀與寫, 中斷打開與關閉(如傳送與接收中斷), 接收狀態處理, 有FIFO時還要處理FIFO的狀態. 如下我們就首先切入這一部分, 具體了解一下與硬體串口IC相關的部分在驅動中的處理, 這一部分可以說是串口驅動中的最基礎部分, 直接與硬體打交道, 完成最底層具體的串口數據傳輸.
1. 串口硬體資源的處理.
    W83697及W83977在ep93xx板子上的映射的硬體物理空間如下:
W83697: 0x20000000起1K空間.
W83977: 0x30000000起1K空間.

    因為串口設備的特殊性, 可以當作終端使用, 但是終端的使用在內核還未完全初始化之前(關於串口與終端的關聯及層次在第四節中詳細), 此時還沒有通過mem_init()建立內核的虛存管理機制, 所以不能通過ioreamp來進行物理內存到虛存的映射(物理內存必須由內核映射成系統管理的虛擬內存后才能進行讀寫訪問), 這與先前所講的framebuffer的物理內存映射是不同的, 具體原因如下:

√終端在註冊並使用的調用路徑如下:
start_kernel→console_init→uart_console_init→ep93xxuart_console_init→register_console→csambuart_console_write.

√FrameBuffer顯卡驅動中的物理內存映射調用路徑如下:
start_kernel→ rest_init→init(內核初始線程)→ do_basic_setup→ do_initcalls→fbmem_init→lanrryfb_init
(Linux下用__setup啟動初期初始機制與__initcall系統初始化完成後的調用機制, 這兩個機制本質沒有什麼差別,主要是執行時所處的系統時段)

√串口物理內存映射到虛存的時機:
依據上面所介紹的兩條執行路徑,再看內核的內存初始化的調用時期,只有完成這個初始化后才能進行物理內存到虛存的映射,內存的初始化主要是在start_kernel中調用的mem_init,這個調用明顯在uart_console_init之後,在fbmem_init之後,到此就全部說明了為何不能在對串口使用ioremap進行物理內存的映射了。那麼究竟要在什麼時機用什麼方法進行串口物理內存的映射呢?

√串口物理內存的映射方式:
參考ep93xx的板載I/O的映射處理,它的處理方式是一次性將所有的物理I/O所在的內存空間映射到虛存空間,映射的基址是IO_BASE_VIRT,大小是IO_SIZE.
/* Where in virtual memory the IO devices (timers, system controllers
* and so on). This gets used in arch/arm/mach-ep93xx/mm.c.*/
#define IO_BASE_VIRT 0xFF000000 // Virtual address of IO
#define IO_BASE_PHYS 0x80000000 // Physical address of IO
#define IO_SIZE 0x00A00000 // How much?

完成映射的函數是ep93xx_map_io, 所有要進行映射內存都在ep93xx_io_desc結構當中描述,我們的串口映射也加在這個地方,基址分別如下:
文件: linux-2.4.21/include/asm-arm/arch-ep93xx/regmap.h
#define IO_W83697_UART_BASE 0x20000000
#define IO_W83697_UART_SIZE 0x1000
#define IO_W83977_UART_BASE 0x30000000
#define IO_W83977_UART_SIZE 0x1000
#define IO_SIZE_2 (IO_SIZE+0x100000)
#define IO_BASE83697_VIRT IO_BASE_VIRT+IO_SIZE
#define IO_BASE83977_VIRT IO_BASE_VIRT+IO_SIZE_2
ep93xx_map_io完成是在arch初始化中賦值給struct machine_desc mdesc這個機器描述結構體,主要由位於mach-ep93xxarch.c文件中如下宏完成此結構的初始化:
MACHINE_START(EDB9302, "edb9302")
…..
MAPIO(ep93xx_map_io) //初始化. map_io= ep93xx_map_io….
MACHINE_END

最終這個函數在調用路徑如下:
start_kernel→setup_arch→paging_init→(mdesc->map_io())
至此完成串口物理內存的映射,這個過程在console_init調用之前,因此不會有問題, 此種方法建立映射是直接創建物理內存頁與虛存頁的對應,大小為4k一頁,最終調用的是create_mapping(), 建立頁表映射是與具體的平台相關的,位於mach_ep93xx/mm/ proc-arm920.S文件中提供了與具體平台相關的頁表建立函數,其中包括TLB表操作/Cache操作/頁表操作等:
在上層的start_kernel→setup_arch→ setup_processor調用下,會在proc-arm920.S文件中查找".proc.info"節的__arm920_proc_info,並從中找到配置的process相關的操作函數,具體的arm頁表建立的詳情須要參看ARM內存管理的相關手冊.
.section ".proc.info", #alloc, #execinstr
.type __arm920_proc_info,#object
__arm920_proc_info:
.long 0x41009200
……
.long arm920_processor_functions
.size __arm920_proc_info, . - __arm920_proc_info

在arm920_processor_functions中包含的頁表操作如下:
/* pgtable */
.word cpu_arm920_set_pgd
.word cpu_arm920_set_pmd
.word cpu_arm920_set_pte

2. 與串口硬體相關的宏主.
如下, 下面將詳術如下, 並指出其具體被使用的環境上下文:
<1>. 讀寫數據.
#define UART_GET_CHAR(p) ((readb((p)->membase + W83697_UARTDR)) & 0xff)
#define UART_PUT_CHAR(p, c) writeb((c), (p)->membase + W83697_UARTDR)

<2>. 接收發送狀態.
#define UART_GET_RSR(p) ((readb((p)->membase + W83697_UARTRSR)) & 0xff)
#define UART_PUT_RSR(p, c) writeb((c), (p)->membase + W83697_UARTRSR)


<3>. 發送及接收中斷狀態.
#define UART_GET_CR(p) ((readb((p)->membase + W83697_UARTCR)) & 0xff)
#define UART_PUT_CR(p,c) writeb((c), (p)->membase + W83697_UARTCR)
#define UART_GET_INT_STATUS(p) ((readb((p)->membase + W83697_UARTIIR)) & 0xff)


<4>. 以及其它的中斷使能設置等, 在傳送時打開傳送中斷即會產生傳送中斷.
#define UART_PUT_ICR(p, c) writeb((c), (p)->membase + W83697_UARTICR)

<5>. FIFO的狀態, 是否讀空/是否寫滿; 每次讀時必須讀至FIFO空, 寫時必須等到FIFO不滿時才能寫(要等硬體傳送完) .
接收中斷讀空FIFO的判斷:
status = UART_GET_FR(port);
while (UART_RX_DATA(status) && max_count--) {
……
}

發送中斷寫FIFO: 當發送緩衝區中有數據要傳送時, 置發送中斷使能, 隨後即產生傳送中斷, 此時FIFO為空, 傳送半個FIFO大小的位元組, 如果發送緩衝區數據傳完,則關閉發送中斷.

<6>. 傳送時可直接寫串口數據口, 而不使用中斷, 但必須等待檢測FIFO的狀態
do {
status = UART_GET_FR(port);
} while (!UART_TX_READY(status)); //wait for tx buffer not full...

3. 串口驅動的參數配置
串口的參數主要包括如下幾個參數,全部都記錄在uart_port結構上,為靜態的賦值,本串口驅動支持6個設備,所以驅動中就包括了6個port,一個串口對應一個port口,他們之間除了對應的中斷號/寄存器起始基址/次設備號不同之外,其它的參數基本相同.

√串口對應中斷, 這裡六個串口,其中有3個串口使用的系統外部中斷0/1/2, 其中另外幾個中斷用提GPIO中斷,具體有關GPIO中斷的內容可參見EP93XX晶元手冊, GPIO中斷共享一個系統中斷向量,涉及中斷共享的問題,後面將詳述LINUX中的中斷共享支持.

√串口時鐘, 串口時鐘用來轉換計算須要設置到配置寄存器當中的波特率比值,其計算方法為:quot = (port->uartclk / (16 * baud)); baud為當前設置的波特率,可為115200等值, 取決於所選的串口時鐘源, quot即為要設置到寄存器當中的比值.

√串口基址, 即串口所有配置寄存器基礎址.

√串口次設備號(由驅動中的最低次設備號依次累加)

前面已經講過了六個串口中斷,這裡詳細列出對應情況如下,方便查找:
w83697的三個串口對應中斷如下:

  • uart 1: 讀寫數據寄存器偏移為00x3F8, 對應系統外部中斷INT_EXT[0].
  • uart 2: 讀寫數據寄存器偏移為00x2F8, 對應系統外部中斷INT_EXT[1].
  • uart 3: 讀寫數據寄存器偏移為00x3e8, 對應系統外部中斷INT_EXT[2].
  • uart 4: 讀寫數據寄存器偏移為00x3e8, 對應EGPIO[8].

w83977的兩個串口對應中斷如下:

  • uart 1: 讀寫數據寄存器偏移為00x3F8, 對應EGPIO[1].
  • uart 2: 讀寫數據寄存器偏移為00x2F8, 對應EGPIO[2].

下面列出其中一個具體的串口port的定義如下:
{
.port = {
.membase = (void *)W83697_UART4_BASE,
.mapbase = W83697_UART4_BASE,
.iotype = SERIAL_IO_MEM,
.irq = W83697_IRQ_UART4, //串口中斷號
.uartclk = 1846100, //uart時鐘,默認.
.fifosize = 8, //硬體fifo大小.
.ops = &amba_pops, //底層驅動的硬體操作集,如開關中斷等.
.flags = ASYNC_BOOT_AUTOCONF,
.line = 3, //串口在次設備數組中的索引號,須注意從0計起…
},
.dtr_mask = 0,
.rts_mask = 0,
}

4. 串口驅動的底層介面函數
驅動文件:linux-2.4.21/drivers/serial/Ep93xx_w83697.c
相關文件: linux-2.4.21/drivers/serial/core.c 下面詳述.
函數: w83697uart_rx_chars(struct uart_port *port, struct pt_regs *regs)
描述: 串口接收數據中斷, 此函數中應當注意的要點如下:

  • 接收數據時,要注意判斷FIFO是否讀空(參見上述2點中說明).
  • 接收數據放入flip緩衝區,此緩衝區專供緩存中斷中接收到的數據,是最原始的串口數據,為更上一層中各種終端處理模式的原始數據,可以進行各種加工處理。
  • 接收數據到flip緩衝區中時,須根據硬體接收狀態,置每一個接收到的字元的接收標誌,放在flag_buf_ptr當中, 標誌類型有TTY_NORMAL/ TTY_PARITY/ TTY_FRAME等,分別表示正常/校驗出錯/幀出錯(無停止位)等.
  • 每接收數據之後,會通過在退出中斷前調用tty_flip_buffer_push()來往tq_timer任務列表中加一個隊列任務,串口的隊列任務主要是負責將中斷接收到flip緩衝區中的數據往上傳輸至終端終沖區, 隊列任務的機制將在後面介紹,它是一種非同步執行機制,在軟中斷中觸發執行.

函數: static void w83697uart_tx_chars(struct uart_port *port)
描述: 串口發送數據中斷, 發送中斷中要做的事比較少,比起接收中斷簡單了好多,注意事項如下:

  • 當上層要發送數據時,就會打開串口發送中斷,此時FIFO為空,傳送半個FIFO大小數據即退出, 通常打開中斷是通過更上一層的uart_flush_chars()調用,最終調用的是w83697uart_start_tx().
  • 檢測當沒有數據要傳輸的時候,關閉傳送中斷,在傳送之前與傳送完之後都有檢測.
  • 最重要的一點是如果傳送緩衝區當中的字元數已經小於WAKEUP_CHARS, 則可以喚醒當前正在使用串口進行傳送的進程,這裡是通過tasklet機制來完成,這也是一非同步執行機制.

順帶介紹開關中斷介面:
static void w83697uart_start_tx(struct uart_port *port, unsigned int tty_start)
static void w83697uart_stop_tx(struct uart_port *port, unsigned int tty_stop)

函數: static void w83697uart_int(int irq, void *dev_id, struct pt_regs *regs)
描述: 中斷處理函數,為3個使用系統外部中斷的的串口的中斷入口,其中必須處理的中斷狀態分為如下幾種, 注意必須在處理中斷時根據手冊中的說明來清除中斷,通常是讀或寫某些寄存器即可。

  • 接收中斷.
  • 傳送中斷.
  • FIFO超時中斷.
  • 其它不具體處理的中斷,必須讀相應寄存器清中斷.

函數: static void w83697uart_int2(int irq, void *dev_id, struct pt_regs *regs)
描述: 中斷處理函數,為另外幾個使用串口使用的GPIO中斷入口,GPIO中斷共享同一個系統中斷向量, 必須根據GPIO的中斷狀態寄存器的相應位來判斷對應的中斷是屬哪一個串口的,從而進行相應的處理,其實這個判斷也是無所謂的,因為中斷產生時傳進來的參數已經含有了相應串口的參數, 在判斷完中斷產生的GPIO口后立即調用w83697uart_int2 完成具體的中斷處理.

函數: static int w83697uart_startup(struct uart_port *port)
描述: 串口開啟后的初始化函數,主要完成初始化配置,以及安裝中斷處理了函數,初始化配置包括打開中斷使能標誌。

函數: static void w83697uart_shutdown(struct uart_port *port)
描述: 串口關閉函數,清除配置,半閉中斷.

函數: static void w83697uart_change_speed(struct uart_port *port, unsigned int cflag, unsigned int iflag, unsigned int quot)
描述: 配置函數,經由上次調用下來,主要配製串口的波特率比,以及各種容錯處理,在串口打開初始化時會被調用,在必變串口波特率/校驗方式/停止位/傳送位數等參數時會被調用.

5. 串口驅動與上層的介面關聯
文件: linux-2.4.21/drivers/serial/core.c
這一層介面是串口驅動中的共用部分代碼, 核心結構為struct uart_driver. 這一層上承TTY終端,下啟串口底層,串口底層主要處理了與串口硬體相關的部分,並向上提供uart中間層向下的介面. Uart coar向下與底層驅動的介面,通過一個static struct uart_ops amba_pops結構完成¸ 這個結構直接賦值給串口struct uart_amba_port amba_ports 的.ops成員,最後將串口的port加入到uart_driver當中完成關聯, 通過uart_add_one_port加入.
static int __init w83697uart_init(void)
{
int ret, i;
ret = uart_register_driver(&amba_reg);
if (ret == 0) {
for (i = 0; i < UART_NR; i++)
uart_add_one_port(&amba_reg, &amba_ports[i].port);
}
return ret;
}

二. Linux的中斷機制及中斷共享機制.
前面講到了有6個串口,除了w83697中的前三個串使用的是獨立的系統外部中斷之外,其它的在個串口是共享一個系統中斷向量的,現在我們來看看多個中斷是如何掛在一個系統中斷向量表當中的,共享中斷到底是什麼樣的一種機制?
進行分析代碼可知,linux下的中斷採用的是中斷向量的方式,每一個中斷對應一個中斷描述數組當中的一項, 結構為struct irqdesc,其當中對應一成員結構為struct irqactionr 的成員action, 這個即表示此中斷向量對應的中斷處理動作,這裡引用從網上下載的一幅圖講明中斷向量表與中斷動作之間的關係:
 
摘自IBM developerWorks 中國
struct irqaction {
void (*handler)(int, void *, struct pt_regs *);
unsigned long flags;
unsigned long mask;
const char *name;
void *dev_id;
struct irqaction *next;
};

從上面的結構體與圖當中,我們就可以很清楚的看到,一個中斷向量表可以對應一個irqaction,也可能對應多個由鏈錶鏈在一起的一個鏈表irqaction, 這當中主要在安裝中斷的時候通過中斷的標誌位來決定:

  • 安裝中斷處理,不可共享:
    retval = request_irq(port->irq, w83697uart_int, 0, "w83697_uart3", port);
  • 安裝中斷處理,可共享:
    retval = request_irq(port->irq, w83697uart_int2, SA_SHIRQ, "w83977_uart5", port);

由上即可知,安裝共享中斷時,只須指定安裝的中斷標誌位flag為SA_SHIRQ, 進入分析安裝中斷的處理可知,在安裝時,會檢測已經安裝的中斷是否支持共享中斷,如果不支持,則新的中斷安裝動作失敗;如果已經安裝的中斷支持共享中斷,則還必須檢測將要安裝的新中斷是否支持中斷共享,如果不支持則安裝還是會失敗,如果支持則將此新的中斷處理鏈接到此中斷向量對應的中斷動作處理鏈表當中.
在產生中斷時,共享中斷向量中對應的中斷處理程序鏈表中的每一個都會被調用,依據鏈表的次序來,這樣處理雖然會有影響到效率,但是一般情況下中斷傳到用戶的中斷處理服務程序中時,由用戶根據硬體的狀態來決定是否處理中斷,所以能常情況下都是立即就返回了,效率的影響不會是大的問題.

三. Linux的軟中斷機制.
前面已經簡單講過了LINUX下的硬中斷處理機制,其實硬中斷的處理都由LINUX底層代碼具體完成了,使用者一般在處理硬中斷時是相當簡單的,只須要用request_irq()簡單的掛上中斷即可,這裡我們進一步介紹一下LINUX下的軟中斷機制,軟中斷機制相比起硬中斷機制稍微複雜一些,而且在LINUX內核本身應用非常的廣, 它作為一種軟性的非同步執行機制,只有深入理解了它才能靈活的運用.
之所以提到內核的softirq機制,主要是因為在串口中斷也使用了這些機制,理解了這些機制就能更加明白串口驅動一些問題, 現在先提出幾個問題如下:

  • 前面提供到中斷接收后數據,先放到flip緩衝區當中,這樣讓人很容易進一步想知道,中斷處理的緩衝區的數據,用戶進程讀取串口時如何讀到的?很明顯中斷處於內核空間,用戶讀取串口輸入進程是在用戶空間,中斷緩衝區中的數據如何被處理到終端緩衝區中,供用戶讀取的?
  • 另外寫串口時,是向終端緩衝區當中寫入,那麼上層的寫操作如何知道下層緩衝區中的的數據是否傳送完成?用戶空間的寫串口進程處於什麼樣的狀態?如果是寫完緩衝區就睡眠以保證高效的CPU使用率,那麼何時才應該醒過來? 由誰負責醒過來?

1. 往tq_timer任務隊列中添加一項任務.
根據以上這兩個問題,我們來深入代碼分析,首先看接收緩衝區中的數據如何上傳, 前面已經提到過,接收中斷處理完成後,會調用tty_flip_buffer_push(),這個函數完成的功能就是往一系統定義的任務隊列當中加入一個任務,下面我們將詳細的分析加入的任務最終是如何執行起來的.[任務:這裡所講的任務可以直接理解成為一個相應的回調函數,LINUX下術語稱作tasklet]
void tty_flip_buffer_push(struct tty_struct *tty)
{
if (tty->low_latency)
flush_to_ldisc((void *) tty);
else
queue_task(&tty->flip.tqueue, &tq_timer);
}

2. tq_timer的執行路徑分析.
tq_timer是一個雙鏈表結構任務隊列,每項任務包含一個函數指針成員, 它通過run_task_queue每次將當中的所有任務(其實是一些函數指針)全部調用一次,然後清空隊列, 最終的執行tq_timer的是在中斷底半的tqueue_bh 中執行,如下:
void tqueue_bh(void)
{
run_task_queue(&tq_timer);
}

在void __init sched_init(void)當中初始化底半的向量如, tqueue_bh初始化在bh_base的TIMER_BH位置,bh_base為一結構很簡單的數組,在什麼位置調用什麼樣的了函數基本已經形成默認的習慣:
init_bh(TIMER_BH, timer_bh);
init_bh(TQUEUE_BH, tqueue_bh);
init_bh(IMMEDIATE_BH, immediate_bh);

看看init_bh相當於初始底半的服務程序,非常簡單:
void init_bh(int nr, void (*routine)(void))
{
bh_base[nr] = routine;
mb();
}

最終真正的執行bh_base中保存的函數指針的,在bh_action()當中:
static void bh_action(unsigned long nr)
{

if (bh_base[nr])
bh_base[nr]();

}

關於這裡所指出的bh_base, 我們在後面就直接稱作bh,意即中斷底半所做的事.

3. tq_timer實現所依賴的tasklet.
那麼bh_action在什麼時候執行呢?bh_action被初始化成bh_task_vec這32個tasklet調用的任務, 因此它的依賴機制是tasklet機制,後面將進行簡單介紹.
void __init softirq_init()
{
int i;
for (i=0; i<32; i++)
tasklet_init(bh_task_vec+i, bh_action, i);
….
}

至此已經把任務隊列的執行流程及原理分析完成,tasklet是須要激活的,這裡我們先指出任務隊列是如何激活的,在時鐘中斷的do_timer()當中會調用mark_bh(TIMER_BH), 來激時鐘底半所依賴運行的tasklet,其中bh_task_vec的所有成員的函數指針全部指向bh_action.
static inline void mark_bh(int nr)
{
tasklet_hi_schedule(bh_task_vec+nr);
}

tasklet_hi_schedule的功能就是往tasklet當中加入一個新的tasklet.

4. tasklet的機制簡單分析.
講到tasklet,我們才與我們真正要講的softirq最近了,因為目前在軟中斷當中有主要的應用就是tasklet,而且在所有32個軟中斷中僅有限的幾個軟中斷如下:
enum{
HI_SOFTIRQ=0,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
TASKLET_SOFTIRQ
};
struct softirq_action{
void (*action)(struct softirq_action *);
void *data;
};

static struct softirq_action softirq_vec[32] __cacheline_aligned; //軟中斷的中斷向量表,實為數組.
[1]. 初始化軟中斷向量.
我們這裡所要講的,就是HI_SOFTIRQ / TASKLET_SOFTIRQ 兩項,據我理解這兩項根本在實現機制上一樣的,之所以分開兩個名字叫主要是為了將不同的功能分開,就類似於雖然同是軟中斷,但是各處所完成的功能不一樣,所以分在兩個軟中斷完成, 後面我們僅取其中用於執行時鐘底半的任務隊列HI_SOFTIRQ為例進行講解, 而且我們不講及多個CPU情況下的tasklet相關機制, 這兩項軟中斷的實始化如下:
void __init softirq_init()
{
….
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}

open_softirq下所做的事相當簡單, 即往軟中斷向量中賦值, 相當於硬中斷當中的request_irq掛硬體中斷:
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
}

[2]. 軟中斷中斷服務程序
對於HI_SOFTIRQ , 相應的中斷服務程序為tasklet_hi_action , 由上文所講的初始化過程給出,這個函數目前完成的功能相當簡單,它的任務就是遍歷執行此中斷所對應一個tasklet鏈表,
NR_CPUS= 1.
struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned;
[3]. 往軟中斷對應的tasklet鏈表中加入新的tasklet, 加在尾部.
void __tasklet_hi_schedule(struct tasklet_struct *t)
{

t->next = tasklet_hi_vec[cpu].list;
tasklet_hi_vec[cpu].list = t;
cpu_raise_softirq(cpu, HI_SOFTIRQ);

}

最重要的一點是,在安裝了新的tasklet后,還必須將軟中斷設置為激活,告訴系統有軟中斷須要執行了,下面一點即提到系統如何檢測是否有軟中斷須要處理:
#define __cpu_raise_softirq(cpu, nr) do { softirq_pending(cpu) |= 1UL << (nr); } while (0)
[4]. 軟中斷所依賴的執行機制.
講到最後還沒有指出軟中斷是如何觸發執行的,其實很簡單:

  • 在系統處理所有硬中斷信號時,他們的入口是統一的,在這個入口函數當中除了執行do_IRQ()完成硬體中斷的處理之外,還會執行do_softirq()來檢測是否有軟中斷須要執行,所以軟中斷所依賴的是硬體中斷機制;
  • 另外還有一個專門處理軟中斷內核線程ksoftirqd(),這個線程處理軟中斷級別是比較低的,他是一個無限LOOP不停的檢測是否有軟中斷須要處理,如果沒有則進行任務調度.

在do_softirq()中有如下的判斷,以決定是否有軟中斷須要執行,如果沒有就直接退出,在[3]中提到的激活軟中斷時,要將相應軟中斷位置1, 軟中斷有32個,因此一個整型數即可以表示32個軟中斷,即可判斷有什麼樣的軟中斷須要處理,代碼如下:
pending = softirq_pending(cpu);
if (pending) {
}
….
do { //檢測32個軟中斷位標誌中是否有為1的…
if (pending & 1)
h->action(h);
h++;
pending >>= 1;
} while (pending);

[4]. 軟中斷所依賴的執行時期問題.
之所以將這個問題單獨列開來講,是因為他特別的重要,上面我已經講過了軟中斷是依賴硬中斷觸發執行的,但是產生如下疑問:

  • 是不是一有硬中斷髮生就會觸發軟中斷的執行?
  • 軟中斷的執行會不會影響到系統的性能?
  • 會不會影響到硬中斷的處理效率?也就是說會不會導致在處理軟中斷時而引起硬中斷無法及時響應呢?

再看do_softirq的代碼當中有如下判斷:
if (in_interrupt())
return;

這個條件就是能否進行軟中斷處理的關鍵條件,因此由此也可以了解到軟中斷是一種優先順序低於硬中斷的軟性機制,具體來看看這個判斷條件是什麼:
/*Are we in an interrupt context? Either doing bottom half
* or hardware interrupt processing?*/

#define in_interrupt() ({ const int __cpu = smp_processor_id();
(local_irq_count(__cpu) + local_bh_count(__cpu) != 0); })
/* softirq.h is sensitive to the offsets of these fields */
typedef struct {
unsigned int __softirq_pending;
unsigned int __local_irq_count;
unsigned int __local_bh_count;
unsigned int __syscall_count;
struct task_struct * __ksoftirqd_task; /* waitqueue is too large */
} ____cacheline_aligned irq_cpustat_t;
#define irq_enter(cpu,irq) (local_irq_count(cpu)++)
#define irq_exit(cpu,irq) (local_irq_count(cpu)--)

看到這裡,不得不再多注意一個結構,那就是irq_cpustat_t, 先前我們講是否有軟中斷產生的標誌位,但沒有提到__softirq_pending,這個變數就是記載32個軟中斷是否產生的標誌,每一個軟中斷對應一個位; 在中斷執行的do_softirq中有如下幾個重要的動作,說明如下:

  • in_interrupt判斷是否可以進行軟中斷處理,判斷的條件就是沒有沒處在硬體中斷環境中,而且還沒有軟中斷正在執行(即不允許軟中斷嵌套),軟中斷的嵌套避免是通過local_bh_disable()/local_bh_enable()實現,至於帶有bh,其意也即指softirq是中斷底半(bh), 在處理硬體中斷時,一進行即會調用irq_enter來表示已經進入硬體中斷處理程序,處理完硬體中斷後再調用irq_exit表示已經完成處理;
  • pending判斷是否有軟中斷須要處理, 每個位用作當作一個軟中斷是否產生的標誌.
  • 清除所有軟中斷標誌位,因為下面即將處理; 但清除之前先緩存起來, 因為下面還要使用這個變數一次.
  • 在進入軟中斷處理后,會關閉bh功能的執行,執行完后才打開,這樣在in_interrupt判斷當中就會直接發現已經有bh在執行,不會再次進入bh執行了,這嚴格保證了bh執行的串列化.
  • 打開硬體中斷,讓軟中斷在有硬體中斷的環境下執行.
  • 處理完軟中斷後關閉硬中斷,再次檢測是否有新的軟中斷產生,如果有的話,卻只須立即處理本次軟中斷過程未發生過的軟中斷向量. 之所以會有新的軟中斷產生,那是因為軟中斷是在開硬體中斷的情況下執行,硬體中斷處理是可能又產生了新的軟中斷. 之所以只處理本次軟中斷未發生的軟中斷向量,依據我自己的理解,其目的是為了不加重軟中斷處理的負擔而不馬上處理,只是相應的喚醒一個wakeup_softirqd線程,這是專門處理軟中斷的,這樣雖然延誤了軟中斷的處理,但避免了在硬中斷服務程序中拖延太長的時間.[關於軟中斷的處理在後緒版本變化也很大,可以進一步學習研究,如何使軟中斷不至影響中斷處理效率]

軟中斷處理這個函數雖然不長,但是相當的關鍵,每一句代碼都很重要,結合上面所說的幾點,與源碼交互起來理解才能根本理解軟中斷的設計機制:
asmlinkage void do_softirq()
{
int cpu = smp_processor_id();
__u32 pending;
unsigned long flags;
__u32 mask;
if (in_interrupt()) return;
local_irq_save(flags);
pending = softirq_pending(cpu);
if (pending) {
struct softirq_action *h;
mask = ~pending;
local_bh_disable();
restart:
/* Reset the pending bitmask before enabling irqs */
softirq_pending(cpu) = 0;
local_irq_enable();
h = softirq_vec;
do {
if (pending & 1)
h->action(h);
h++;
pending >>= 1;
} while (pending);
local_irq_disable();
pending = softirq_pending(cpu);
if (pending & mask) {
mask &= ~pending;
goto restart;
}
__local_bh_enable();
if (pending)
wakeup_softirqd(cpu);
}
local_irq_restore(flags);
}
}

四. TTY與串口的具體關聯.
串口設備可以當作TTY終端來使用,這又使串口設備比一般的設備稍微複雜一些,因為他還必須與終端驅動關聯起來,雖然這部分與TTY的關聯已經是屬於公用部分的代碼,並不須要驅動編寫者特別做些什麼來進行支持,但對它與TTY的層次關聯的了解有助於理解整個串口的數據流向.
串口要能夠成為終端,必須客外加入終端註冊及初始化的代碼,這部分很簡單,基本上所有的串口驅動都是固定的模式,並無什麼修改,主要包括如下結構:
static struct console cs_amba_console = {
.name = "ttyBM",
.write = w83697uart_console_write,
.device = w83697uart_console_device,
.setup = w83697uart_console_setup,
.flags = CON_PRINTBUFFER,
.index = -1,
};

串口終端的註冊通過下面的函數,將cs_amba_console註冊成為終端, 這個函數調用路徑是:
start_kernel()→console_init()→ep93xxuart_w83697_console_init()
void __init ep93xxuart_w83697_console_init(void)

終端會對應一種具體設備的driver, 相對於串口這個結構是uart_driver, 在驅動中我們已經提供了一個這樣的結構. static struct uart_driver amba_reg, uart_register_driver會將它註冊成為終端對應的driver, 因此真正串口與終端的關聯就在此處建立.
函數: static int __init w83697uart_init(void)
描述: 調用uart_register_driver()完成串口與終端的關聯,將串口註冊成為一種TTY設備,在uart_register_driver()當中調用tty_register_driver()完成TTY設備註冊; 其次是完成串口port口的註冊,將靜態描述的所有串口port(結構為struct uart_port)註冊到uart_driver當中.
特別說明: 註冊串口TTY設備時,由於歷史的原因會註冊兩個TTY設備,一個是normal, 另一個是callout, 是兩個設備來的, 在我們這裡兩者沒有什麼差別,請看源碼中的註解:
.normal_name = "ttyBM",
.callout_name = "cuaam",
/*
* The callout device is just like the normal device except for
* the major number and the subtype code.
*/

函數: static void __exit w83697uart_exit(void)
描述: 卸截設備,卸截port口,因為我編譯的驅動是與內核綁定在一起的,因此實際上根本不會調用此函數.




[admin via 研發互助社區 ] linux UART串口驅動開發文檔已經有2813次圍觀

http://cocdig.com/docs/show-post-42117.html