前言
控制臺,做為一種人機交互接口,相較于其他接口(顯示器、網(wǎng)絡(luò)終端),可能是最簡單的。它耗用資源少,容易配置,幾乎是任何芯片會自帶的外設(shè)。而且可以很容易和計算機建立連接。因而,串口控制臺可能是程序員進行人機交互的首選。
啟用控制臺,可以幫我們在系統(tǒng)全速運行時窺探系統(tǒng)運行狀況。可以監(jiān)測其它外設(shè)或者組件初始化過程。
一月份,筆者在論壇發(fā)的 serialX 串口驅(qū)動反響很大,應(yīng)該會讓很多人眼前一亮(老王賣瓜)。當(dāng)初,決定費力研究它的初衷很簡單 —— 應(yīng)用和驅(qū)動弱耦合、真阻塞非阻塞特性。這兩個月來,筆者一直在實際項目中使用它,而且應(yīng)用到了控制臺上,同時發(fā)現(xiàn)了一些問題。
控制臺串口問題匯總
問題一、任務(wù)調(diào)度器啟動前 `rt_kprintf` 死循環(huán)到 tx 函數(shù)
問題原因是,筆者打開控制臺串口設(shè)定的 flag 是阻塞寫方式,無論是中斷發(fā)送還是 DMA 發(fā)送都依賴中斷。但是任務(wù)調(diào)度器啟動前是關(guān)全局中斷的,這樣導(dǎo)致觸發(fā)發(fā)送失敗,發(fā)送緩沖區(qū)滿了以后再有寫動作就會永久死到 tx 函數(shù)里。
> 應(yīng)對之策:任務(wù)調(diào)度器啟動前只能使用 poll 發(fā)送模式。任務(wù)調(diào)度器啟動后或臨啟動時切換到中斷或 DMA 模式。
問題二、出現(xiàn)異常后,`rt_kprintf` 無輸出
這個現(xiàn)象和上一條有點兒類似。不同的是,雖然全局中斷是開著的,但是串口中斷優(yōu)先級不足,導(dǎo)致控制臺設(shè)備停止工作。
在 arm9 架構(gòu)上出現(xiàn) Undef SWI PAbt DBbt 等等 trap 后,串口外設(shè)中斷級別不足,導(dǎo)致這些 trap 中的 printf 輸出失效。
> 應(yīng)對之策:unset 控制臺串口設(shè)備,或者將控制臺串口切換到 poll 發(fā)送模式。
問題三、`rt_hw_interrupt_disable` 之后 `rt_kprintf` 無輸出
這個和“問題一”是一樣的,根本原因是,非 poll 模式必須有中斷它才能工作,關(guān)中斷以后設(shè)備停止工作。
> 應(yīng)對之策:盡量減少關(guān)中斷時間;避免在關(guān)中斷之后寫串口設(shè)備。
問題四、遇到 `_rt_scheduler_stack_check` 也會停止輸出
因為 `_rt_scheduler_stack_check` 函數(shù)最后先關(guān)全局中斷,然后進入 while 死循環(huán)。這個時候串口中斷肯定也失效了。
> 應(yīng)對之策:關(guān)全局中斷前,先 flush 串口設(shè)備。讓串口把 “stack overflow” 的提示信息輸出完。
問題五、打斷點后 `rt_kprintf` 輸出不完整,部分數(shù)據(jù)沒輸出到控制臺
因為 debug 斷點停止的時候,前邊 printf 緩存的數(shù)據(jù)可能還沒來得及送到串口移位寄存器,cpu 的時鐘被斷點打斷停止運行了,導(dǎo)致部分數(shù)據(jù)沒輸出。繼續(xù)運行程序就可以出現(xiàn)剩余信息輸出。這是非阻塞設(shè)備的特性。
以上這些問題是所有非 poll 非阻塞設(shè)備輸出都會遇到的現(xiàn)象。在 RTOS 系統(tǒng)里,應(yīng)用程序不可避免地要和中斷打交道,了解中斷對我們編程思想的影響很重要。
完整解決方案
rt_device 增加 flush 接口
flush 接口對帶緩存設(shè)備是極其有用的,無論是阻塞還是非阻塞模式,我們總有需求要求*在某個代碼節(jié)點設(shè)備的緩存已經(jīng)是空的*,*或者要求實現(xiàn)通信同步*。
`struct rt_device` 增加 `flush` 接口
struct rt_device
{
...
rt_err_t (*flush) (rt_device_t dev);
...
};
serialX.c 添加 `flush` 回調(diào)函數(shù)實現(xiàn) `static rt_err_t rt_serial_flush(struct rt_device *dev)` ,用于等待串口驅(qū)動層發(fā)送緩存發(fā)完數(shù)據(jù)。另外底層外設(shè)也增加 flush 接口,用于等待串口發(fā)送寄存器中的*最后一個字節(jié)數(shù)據(jù)*被搬到了移位發(fā)送寄存器中。
console 添加 unset flush 控制臺設(shè)備接口
void rt_console_unset_device()
{
if (_console_device != RT_NULL)
{
/* close old console device */
rt_device_close(_console_device);
_console_device = RT_NULL;
}
}
RT_WEAK void rt_hw_console_flush()
{
/* empty console output */
}
void rt_console_flush()
{
#ifdef RT_USING_DEVICE
if (_console_device == RT_NULL)
{
rt_hw_console_flush();
}
else
{
rt_device_flush(_console_device);
}
#else
rt_hw_console_flush();
#endif /* RT_USING_DEVICE */
}
有 set 也有 unset, 不是嗎? unset 是為了調(diào)用 `rt_hw_console_output` 而不是 `rt_device_write` 輸出打印信息。
`rt_console_flush` 既考慮啟用設(shè)備框架也考慮未啟用設(shè)備框架兩種情況。`rt_device_flush(_console_device)` 會調(diào)用上文的 `rt_serial_flush` ;`rt_hw_console_flush` 和 `rt_hw_console_output` 類似用于不使用設(shè)備框架,自定義 `rt_kprintf` 底層接口時要實現(xiàn)的。視實際情況實現(xiàn) `rt_hw_console_flush` 。例如 NUC970 UART 自帶了 FIFO ,需要實現(xiàn) `rt_hw_console_flush`
> 如果使用了 DMA 模式,底層實現(xiàn) flush 還是有點兒難度的。需要花點兒心思。
延遲 `rt_console_set_device` 調(diào)用
挪到任務(wù)調(diào)度器啟動前,那么之前的控制臺輸出怎么實現(xiàn)?答案是使用 `rt_hw_console_output`。如上所說,第一次使用 poll 模式打開控制臺串口,到這里臨啟動任務(wù)調(diào)度器的時候再次用 中斷/DMA 模式打開控制臺串口也可以。但是,多次用不同模式打開同一個設(shè)備會引入另外的問題,要不要先關(guān)閉上次的 open 呢?假如之前沒有打開過呢?
/* Set the shell console output device */
#ifdef RT_USING_CONSOLE
rt_console_flush();
rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif
/* start scheduler */
rt_system_scheduler_start();
這時候,我們的 `rt_console_set_device` 可以用任何模式打開控制臺串口設(shè)備
if (rt_device_open(new_device, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_STREAM
| RT_DEVICE_FLAG_INT_RX
| RT_DEVICE_FLAG_INT_TX
) == RT_EOK) {
_console_device = new_device;
}
或者,先用 poll 模式 set console device
/* set new console device */
if (rt_device_open(new_device, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_STREAM
) == RT_EOK) {
_console_device = new_device;
}
當(dāng)?shù)诙?reset 的時候,需要先 unset (用到了上面提到的 `rt_console_unset_device`),因為 `rt_console_set_device` 不允許重復(fù) set 同一個設(shè)備,也沒法修改打開設(shè)備的參數(shù)。寫另外一個 set api 也就變的必要了
if (rt_device_open(new_device, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_STREAM
| RT_DEVICE_FLAG_INT_RX
| RT_DEVICE_FLAG_INT_TX
) == RT_EOK) {
_console_device = new_device;
}
但是,我為什么不喜歡這種方式呢?
1. board 初始化階段需要初始化系統(tǒng)時鐘、倍頻 cpu 時鐘、 `rt_hw_systick_init`、 `rt_system_heap_init`、 `rt_hw_pin_init`、還有 `rt_hw_usart_init` 設(shè)備,可能還有 `rt_console_set_device`。為了能第一時間使用上控制臺串口,`rt_hw_usart_init` 必須盡早執(zhí)行,然后是 `rt_console_set_device` 。
2. 但是 uart 設(shè)備可能用到動態(tài)申請內(nèi)存,這樣就必然要求 `rt_system_heap_init` 先于 `rt_hw_usart_init` 。
3. 初始化系統(tǒng)時鐘、倍頻 cpu 時鐘、 `rt_hw_systick_init`、 `rt_system_heap_init`、 `rt_hw_usart_init` 。也只能這樣了,前邊幾步的串口打印需求就忽略了吧,`rt_system_heap_init`->`rt_memheap_init` 里的 `RT_DEBUG_LOG` 調(diào)試信息就忽略了吧。
如果不著急用串口設(shè)備,先簡單初始化串口外設(shè),讓 `rt_hw_console_output` 以最快的速度工作起來。如此一來,初始化流程可能就可以變成,初始化系統(tǒng)時鐘、倍頻 cpu 時鐘、 **`rt_hw_console_init`**、 `rt_hw_systick_init`、... 。后面是初始化順序都無關(guān)緊要了,而且所有的打印信息需求都可以滿足。最后在任務(wù)調(diào)度器啟動前選擇某個串口設(shè)備做控制臺串口,將會避免前文說到的*問題一*。
進入不可恢復(fù)狀態(tài)的處理
- 以 `_rt_scheduler_stack_check` 為例,關(guān)中斷前先 flush 控制臺。
rt_console_flush();
level = rt_hw_interrupt_disable();
while (level);
- 還比如 SWI 異常,`rt_hw_cpu_shutdown` 也會關(guān)中斷,進入 while 死循環(huán)。先 unset 控制臺,使用 `rt_hw_console_output` 進行 poll 輸出之后的輸出需求。
void rt_hw_trap_swi(struct rt_hw_register *regs)
{
rt_console_unset_device();
rt_hw_show_register(regs); rt_kprintf("software interrupt\n");
rt_hw_cpu_shutdown();
}
**注:為避免因中斷優(yōu)先級,引起串口設(shè)備中斷得不到響應(yīng),在中斷響應(yīng)里切忌調(diào)用 `rt_console_flush` 函數(shù)**
結(jié)束
控制臺串口在系統(tǒng)中扮演著極其重要的角色,對其處理不當(dāng),會引起各種依賴問題。有人就有疑慮了,做其它通信用時 serialX 會不會存在同樣的隱患?筆者保證,您不在中斷響應(yīng)里調(diào)用 `rt_device_flush` 就不會出現(xiàn)以上所有列出來的問題。
筆者下一篇計劃聊聊內(nèi)核啟動流程的問題,雖然之前發(fā)過一篇文章 [rt-thread 系統(tǒng)啟動及 SysTick 初始化流程優(yōu)化可行性分析]( https://club.rt-thread.org/ask/article/2881.html ),里面提了一種可能的系統(tǒng)啟動流程,當(dāng)時只是一種想法,并不系統(tǒng)。再寫一篇,筆者希望把需要考慮的問題以及優(yōu)缺點系統(tǒng)化地說明白,可能還會提及控制臺串口設(shè)備,以及控制臺對內(nèi)核啟動流程的影響。
相關(guān)文章:
rt-thread 驅(qū)動篇(一) serialX 框架理論
rt-thread 驅(qū)動篇(二) serialX 理論實現(xiàn)
rt-thread 驅(qū)動篇(三) serialX 壓力測試
rt-thread 驅(qū)動篇(四)serialX 多架構(gòu)適配
rt-thread 驅(qū)動篇(五)serialX 小試牛刀
審核編輯:湯梓紅
-
人機交互
+關(guān)注
關(guān)注
12文章
1217瀏覽量
55535 -
控制臺
+關(guān)注
關(guān)注
0文章
85瀏覽量
10411 -
串口
+關(guān)注
關(guān)注
14文章
1558瀏覽量
77072 -
RT-Thread
+關(guān)注
關(guān)注
31文章
1305瀏覽量
40392 -
serialX
+關(guān)注
關(guān)注
0文章
7瀏覽量
811
發(fā)布評論請先 登錄
相關(guān)推薦
RT-Thread記錄(一、版本開發(fā)環(huán)境及配合CubeMX)
![<b class='flag-5'>RT-Thread</b>記錄(一、版本開發(fā)環(huán)境及配合CubeMX)](https://file.elecfans.com//web2/M00/4C/58/pYYBAGKtyOGACMdxAABXG3HKIdk106.png)
RT-Thread NUC97x 移植 LVGL
基于RT-Thread的SPI通訊
RT-Thread設(shè)備驅(qū)動開發(fā)指南基礎(chǔ)篇—以先楫bsp的hwtimer設(shè)備為例
![<b class='flag-5'>RT-Thread</b>設(shè)備<b class='flag-5'>驅(qū)動</b>開發(fā)指南基礎(chǔ)<b class='flag-5'>篇</b>—以先楫bsp的hwtimer設(shè)備為例](https://file1.elecfans.com/web2/M00/C1/40/wKgaomXUXOGAMhesAAAk3OUcwHA076.png)
RT-Thread驅(qū)動開發(fā)指南進階篇-動手驅(qū)動先楫未適配的外設(shè)LCD
![<b class='flag-5'>RT-Thread</b><b class='flag-5'>驅(qū)動</b>開發(fā)指南進階<b class='flag-5'>篇</b>-動手<b class='flag-5'>驅(qū)動</b>先楫未適配的外設(shè)LCD](https://file1.elecfans.com/web2/M00/C1/D4/wKgaomXarr-AKhdfAAAcu6ZeWvU306.png)
RT-Thread編程指南
RT-Thread Studio驅(qū)動SD卡
![<b class='flag-5'>RT-Thread</b> Studio<b class='flag-5'>驅(qū)動</b>SD卡](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
RT-Thread學(xué)習(xí)筆記 RT-Thread的架構(gòu)概述
![<b class='flag-5'>RT-Thread</b>學(xué)習(xí)筆記 <b class='flag-5'>RT-Thread</b>的架構(gòu)概述](https://file.elecfans.com/web2/M00/52/31/pYYBAGLKk5WAA__jAADjdAdXhIs410.jpg)
《RT-Thread設(shè)備驅(qū)動開發(fā)指南》基礎(chǔ)篇--以先楫bsp的hwtimer設(shè)備為例
![《<b class='flag-5'>RT-Thread</b>設(shè)備<b class='flag-5'>驅(qū)動</b>開發(fā)指南》基礎(chǔ)<b class='flag-5'>篇</b>--以先楫bsp的hwtimer設(shè)備為例](https://file.elecfans.com/web2/M00/37/D7/pYYBAGI9l9uAOwALAAAmFmqVYdg094.png)
評論