前言:苦串口驅動久矣!
現狀
串口驅動三種工作模式:輪詢、中斷、DMA。
輪詢模式占用 CPU 最高,但是實現也是最簡單的;DMA 占用 CPU 最少,實現也是最麻煩的;中斷模式居中。
原串口驅動有以下幾個問題:
1、中斷模式,接收有緩存,發送沒緩存
2、中斷模式,讀操作是非阻塞的,沒有阻塞讀;寫操作因為沒有緩存,只能阻塞寫,沒有非阻塞寫。
3、中斷接收過程,每往發送寄存器填充一個字符,就使用完成量等待發送完成中斷,通過完成量進行進程調度次數和發送數據量同樣多!
4、DMA 模式比較復雜,在實現上更復雜。
a.首先,接收有兩種緩存方案,一種沒有緩存,借用應用層的內存直接做 DMA 接收緩存;一種有緩存,用的和中斷模式下相同的 fifo 數據結構。發送只有一種緩存方式,把應用層內存放到數據隊列里做發送緩存。
b.無論哪種緩存方案,都沒有考慮阻塞的問題。而是拋給串口驅動一個內存地址,就返回到應用層了。應用層要么動用rt_device_set_rx_indicate
rt_device_set_tx_complete
做同步——退化成 poll 模式,失去了 DMA 的優勢;要么繼續干其它工作——拋給串口驅動的內存可能引入隱患。
c.為了防止 DMA 工作的時候又有新的讀寫需求。
對串口驅動的期望
輪詢模式不在今天討論計劃內。下面所有的討論都只涉及中斷和 DMA 兩種模式。
無論哪種工作模式,都應該有至少一級緩存機制。
無論哪種工作模式,都應該可以設置成阻塞或者非阻塞。
默認是阻塞 io 模式;如果想用非阻塞工作模式,可以通過 open 或者 control 修改。
讀寫阻塞特性是同步的,不存在阻塞寫非阻塞讀或者非阻塞寫阻塞讀兩種模式。
阻塞讀的過程是,沒有數據永久阻塞;有數據無論多少(小于等于期望數據量),返回讀取的數據量。
阻塞寫的過程是,緩存空間為 0 阻塞等待緩存被釋放;緩存空間不足先填滿緩存,繼續等待緩存被釋放;緩存空間足夠,把應用層數據拷貝到驅動緩存。最后返回搬到緩存的數據量。
非阻塞讀的過程是,沒有數據返回 0;有數據,從 fifo 拷貝數據到應用層提供的內存,返回拷貝的數據量。
非阻塞寫的過程是,緩存為 0 ,返回 0;緩存不足返回寫成功了多少數據;緩存足夠,把數據搬移完,返回寫成功的數據量。
無論是輪詢、中斷、DMA 哪種模式,都應該可以實現 STREAM 特性。
中斷模式下的理論實踐
注:以下實現是在 NUC970 上完成的,有些特性可能不是通用的。例如,串口外設自帶硬件 fifo ,uart1 是高速 uart 設備,fifo 有 64 字節。uart3 的 fifo 就只有 16 字節。
定義緩存數據結構
為實現上述需求,接收和發送都需要有如下一個 fifo
1structrt_serial_fifo
2{
3rt_uint32_tbuf_sz;
4/*softwarefifobuffer*/
5rt_uint8_t*buffer;
6
7rt_uint16_tput_index,get_index;
8
9rt_bool_tis_full;
10};
注:別問我為啥不用 ringbuffer
大部分還是借用struct rt_serial_rx_fifo
的實現的。增加了個buf_sz
由 fifo 自己維護自己的緩存容量
針對 fifo 特意定義了三個函數,
rt_forceinline rt_size_t _serial_fifo_calc_data_len(struct rt_serial_fifo *fifo)
計算 fifo 中寫入的數據量
rt_forceinline void _serial_fifo_push_data(struct rt_serial_fifo *fifo, rt_uint8_t ch)
壓入一個數據(不完整實現,具體見下文)
rt_forceinline rt_uint8_t _serial_fifo_pop_data(struct rt_serial_fifo *fifo)
彈出一個數據(不完整實現,具體見下文)
讀設備過程
讀設備對應中斷接收。
1rt_inlineint_serial_int_rx(structrt_serial_device*serial,rt_uint8_t*data,intlength)
2{
3rt_size_tlen,size;
4structrt_serial_fifo*rx_fifo;
5rt_base_tlevel;
6
7RT_ASSERT(serial!=RT_NULL);
8
9rx_fifo=(structrt_serial_fifo*)serial->serial_rx;
10RT_ASSERT(rx_fifo!=RT_NULL);
11
12/*disableinterrupt*/
13level=rt_hw_interrupt_disable();
14
15len=_serial_fifo_calc_data_len(rx_fifo);
16
17if((len==0)&&//non-blockingiomode
18(serial->parent.open_flag&RT_DEVICE_OFLAG_NONBLOCKING)==RT_DEVICE_OFLAG_NONBLOCKING){
19/*enableinterrupt*/
20rt_hw_interrupt_enable(level);
21return0;
22}
23if((len==0)&&//blockingiomode
24(serial->parent.open_flag&RT_DEVICE_OFLAG_NONBLOCKING)!=RT_DEVICE_OFLAG_NONBLOCKING){
25do{
26/*enableinterrupt*/
27rt_hw_interrupt_enable(level);
28
29rt_completion_wait(&(serial->completion_rx),RT_WAITING_FOREVER);
30
31/*disableinterrupt*/
32level=rt_hw_interrupt_disable();
33
34len=_serial_fifo_calc_data_len(rx_fifo);
35}while(len==0);
36}
37
38if(len>length){
39len=length;
40}
41
42/*readfromsoftwareFIFO*/
43for(size=0;size44{
45/*otherwisethere'sthedata:*/
46*data=_serial_fifo_pop_data(rx_fifo);
47data++;
48}
49
50rx_fifo->is_full=RT_FALSE;
51
52/*enableinterrupt*/
53rt_hw_interrupt_enable(level);
54
55returnsize;
56}
簡單說明就是:關中斷,計算緩存數據量,如果為空判斷是否需要阻塞??截愅陻祿?,開中斷。
這里需要注意的是,拷貝完數據后 fifo 必然不會是 full 的,rx_fifo->is_full = RT_FALSE
這句沒有加在_serial_fifo_pop_data
函數,所以上面說它的實現是不完整的。
寫設備過程
寫設備對應中斷發送
1rt_inlineint_serial_int_tx(structrt_serial_device*serial,constrt_uint8_t*data,intlength)
2{
3rt_size_tlen,length_t,size;
4structrt_serial_fifo*tx_fifo;
5rt_base_tlevel;
6rt_uint8_tlast_char=0;
7
8RT_ASSERT(serial!=RT_NULL);
9
10tx_fifo=(structrt_serial_fifo*)serial->serial_tx;
11RT_ASSERT(tx_fifo!=RT_NULL);
12
13size=0;
14do{
15length_t=length-size;
16/*disableinterrupt*/
17level=rt_hw_interrupt_disable();
18
19len=tx_fifo->buf_sz-_serial_fifo_calc_data_len(tx_fifo);
20
21if((len==0)&&//non-blockingiomode
22(serial->parent.open_flag&RT_DEVICE_OFLAG_NONBLOCKING)==RT_DEVICE_OFLAG_NONBLOCKING){
23/*enableinterrupt*/
24rt_hw_interrupt_enable(level);
25break;
26}
27
28if((len==0)&&//blockingiomode
29(serial->parent.open_flag&RT_DEVICE_OFLAG_NONBLOCKING)!=RT_DEVICE_OFLAG_NONBLOCKING){
30/*enableinterrupt*/
31rt_hw_interrupt_enable(level);
32
33rt_completion_wait(&(serial->completion_tx),RT_WAITING_FOREVER);
34
35continue;
36}
37
38if(len>length_t){
39len=length_t;
40}
41/*copytosoftwareFIFO*/
42while(len>0)
43{
44/*
45*tobepolitewithserialconsoleaddalinefeed
46*tothecarriagereturncharacter
47*/
48if(*data=='
'&&
49(serial->parent.open_flag&RT_DEVICE_FLAG_STREAM)==RT_DEVICE_FLAG_STREAM&&
50last_char!='
')
51{
52_serial_fifo_push_data(tx_fifo,'
');
53
54len--;
55if(len==0)break;
56last_char=0;
57}elseif(*data=='
'){
58last_char='
';
59}else{
60last_char=0;
61}
62
63_serial_fifo_push_data(tx_fifo,*data);
64
65data++;len--;size++;
66}
67
68/*ifthenextpositionisreadindex,discardthis'readchar'*/
69if(tx_fifo->put_index==tx_fifo->get_index)
70{
71tx_fifo->is_full=RT_TRUE;
72}
73
74//TODO:starttx
75serial->ops->start_tx(serial);
76
77/*enableinterrupt*/
78rt_hw_interrupt_enable(level);
79}while(size80
81returnsize;
82}
簡單說明就是:關中斷,計算 fifo 剩余容量,如果空間不足判斷是否阻塞??截悢祿_中斷。
如果數據沒拷貝完,繼續上述過程,直到所有數據拷貝完成。
上述函數也實現了 STREAM 打開模式,檢查 “r”“n” 不完整的問題。
特別注意:上述函數并沒有執行寫“發送寄存器”的操作,開中斷前,這里執行了一句serial->ops->start_tx(serial)
用于開啟發送過程(這個的實現可能在不同芯片上略有差異)。
中斷接收
1while(1){
2ch=serial->ops->getc(serial);
3if(ch==-1)break;
4
5/*iffifoisfull,discardonebytefirst*/
6if(rx_fifo->is_full==RT_TRUE){
7rx_fifo->get_index+=1;
8if(rx_fifo->get_index>=rx_fifo->buf_sz)rx_fifo->get_index=0;
9}
10/*pushanewdata*/
11_serial_fifo_push_data(rx_fifo,ch);
12
13/*ifputindexequaltoreadindex,fifoisfull*/
14if(rx_fifo->put_index==rx_fifo->get_index)
15{
16rx_fifo->is_full=RT_TRUE;
17}
18}
19
20rt_completion_done(&(serial->completion_rx));
先計算是否還有數據要發送,如果沒有,調用serial->ops->stop_tx(serial)
對應上面的serial->ops->start_tx(serial)
。
因為硬件自帶 fifo ,這里最多可以連續寫 64 個字節。
因為發送 fifo 是往外彈出數據的,最后肯定是非滿的。
未說明的問題
對于串口設備來講,接收是非預期的,所以串口接收中斷必須一直開著。發送就不一樣了,沒有發送數據的時候是可以不開發送中斷的。
上文中提到的兩個opsstart_tx
stop_tx
正是開發送中斷使能,關發送中斷使能。另外,它倆還有更重要的作用。
在 NUC970 的設計上,只要發送寄存器為空就會有發送完成中斷,并不是發送完最后一個字節才產生。正因為這個特性,當開發送中斷使能的時候會立馬進入中斷。在中斷里判斷是否有數據要發送,剛好可以作為“啟動發送”。
對于其它芯片,如果發送中斷的含義是“發送完最后一個字節”,僅僅使能發送中斷還不夠,還需要軟件觸發發送中斷。這是發送不同于接收的最重要的地方。
DMA 模式下的實現探討
為什么上一節叫實踐,這一節變成探討了?
第一,筆者還沒時間在 NUC970 上完成 DMA 的部分。
第二,有了上面中斷模式的鋪墊,DMA 模式也是輕車熟路。不覺得 NUC970 的硬件 fifo 就是 DMA 的翻版嗎?
DMA 模式需要二級緩存機制。第一級緩存和中斷模式用的 fifo 一樣。這樣 read write 兩個函數的實現可以是一樣的。
在此基礎上,增加一個數組。如下是完整串口設備定義:
1structrt_serial_device
2{
3structrt_deviceparent;
4
5conststructrt_uart_ops*ops;
6structserial_configureconfig;
7
8void*serial_rx;
9void*serial_tx;
10
11rt_uint8_tserial_dma_rx[64];
12rt_uint8_tserial_dma_tx[64];
13
14cb_serial_tx_cb_tx;
15cb_serial_rx_cb_rx;
16
17structrt_completioncompletion_tx;
18structrt_completioncompletion_rx;
19};
20typedefstructrt_serial_devicert_serial_t;
這兩個數組作為 DMA 收發過程的緩存。
發送數據時,從 serial_tx 的 fifo 拷貝數據到 serial_dma_tx ,啟動 DMA。發送完成后判斷 serial_tx 的 fifo 是否還有數據,有數據繼續拷貝,直到 fifo 為空關閉 DMA 發送。
接收數據時,在 DMA 中斷里拷貝serial_dma_rx
所有數據到 serial_rx 的 fifo 。如果 DMA 中斷分完成一半中斷和全部傳輸完成兩種中斷。可以分成兩次中斷,每次只處理一半數據,這樣每次往 fifo 倒騰數據的時候,還有一半緩沖區可用,也不至于會擔心倉促。
我們需要做的工作只有“怎么安全有效啟動 DMA 發送。
底層驅動
以上都是串口設備驅動框架部分,下面說說和芯片操作緊密相關的部分
init 函數,負責注冊設備到設備樹。
configure 函數,負責串口外設初始化,包括波特率、數據位、流控等等。還有個重要的工作就是調用引腳復用配置函數。
control 函數,使能禁用收發等中斷。
putc 函數,負責寫發送寄存器,寫寄存器前一定先判斷發送寄存器是否可寫是否為空,阻塞等。
getc 函數,負責讀接收寄存器,讀寄存器前一定先判斷是否有有效數據,如果沒有返回 -1。
start_tx 函數,使能發送中斷,如果發送寄存器為空,觸發發送中斷。(如果芯片沒有這個特性,需要想辦法觸發發送完成中斷)
stop_tx 函數,禁用發送中斷。
中斷回調函數,負責處理中斷,根據中斷狀態調用rt_hw_serial_isr
函數。
實機驗證
中斷模式在 NUC970 芯片下經過千萬級數據收發測試的考驗。測試環境有如下兩種:
1、非阻塞 io;波特率 9600;串口調試工具:USR-TCP232 ,USR 出的調試工具。
串口調試工具定時 50ms 發送 30 個字符。NUC970 接收到數據后返回接收到的數據。
2、阻塞 io;波特率 115200;串口調試工具:USR-TCP232 ,USR 出的調試工具。
串口調試工具定時 10ms 發送 30 個字符。NUC970 接收到數據后返回接收到的數據。(串口調試助手發送了 200w 字節數據,接收到了相同個數字符!)
結論
因為 NUC970 芯片的特殊性,上面雖說使用的是中斷模式,其實和 DMA 有點兒類似了。假如是沒收發一個字節數據各對應一次中斷,中斷次數會比較多。
但是,在應用層來看,無論是中斷還是 DMA 都是一樣的——要么阻塞,要么非阻塞。
原文標題:RT-Thread驅動篇之串口驅動框架剖析及性能提升
文章出處:【微信公眾號:RTThread物聯網操作系統】歡迎添加關注!文章轉載請注明出處。
審核編輯:湯梓紅
-
cpu
+關注
關注
68文章
10899瀏覽量
212607 -
驅動
+關注
關注
12文章
1848瀏覽量
85466 -
串口
+關注
關注
14文章
1557瀏覽量
76828
原文標題:RT-Thread驅動篇之串口驅動框架剖析及性能提升
文章出處:【微信號:RTThread,微信公眾號:RTThread物聯網操作系統】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論