單片機(jī)串口控制臺(tái)方案

通過串口使用類似Linux命令的方式控制單片機(jī),大幅提高調(diào)試效率,可以在非debug模式下實(shí)現(xiàn)類似debug的效果
網(wǎng)上找了找類似的東西,好像資料不是很多的樣子,F(xiàn)reeRTOS提供了解決方案,RTthread也有FinSH控制臺(tái),但是感覺有點(diǎn)復(fù)雜,所以自己做了一個(gè),非常輕量,快速,沒有多余的功能

????????串口接收部分使用FreeRTOS操作系統(tǒng)+任務(wù)通知+DMA+空閑中斷實(shí)現(xiàn)(參考:CH32串口接收方案),當(dāng)然也可以自己使用其他的方式去實(shí)現(xiàn)
效果展示
在終端中輸入? list,用于顯示當(dāng)前已注冊(cè)命令

在終端中輸入 info,顯示當(dāng)前已有任務(wù)的剩余堆棧,這對(duì)于合理分配任務(wù)大小很有幫助

大致工作流程
????????首先初始化控制臺(tái),主要是啟動(dòng)串口接收,因?yàn)榭刂婆_(tái)使用了動(dòng)態(tài)單向鏈表,所以還需要為頭節(jié)點(diǎn)分配內(nèi)存,初始化完成后即可將自己需要的命令字和功能回調(diào)函數(shù)注冊(cè)進(jìn)去(命令字和功能函數(shù)是一一對(duì)應(yīng)的,每個(gè)命令都對(duì)應(yīng)了一個(gè)特定的功能函數(shù)),之后由串口接收任務(wù)等待上位機(jī)命令,接收到命令后根據(jù)命令去調(diào)用指定的功能函數(shù)即可

詳解
一、?相關(guān)定義(.h)
console.h
????????首先需要一個(gè)函數(shù)指針類型,這個(gè)類型就是注冊(cè)進(jìn)系統(tǒng)的功能回調(diào)函數(shù)的類型,無返回值,有一個(gè)char* 類型的參數(shù),可以用于接收上位機(jī)發(fā)送的指令,進(jìn)行參數(shù)解析,用于實(shí)現(xiàn)類似于“setv -1 -200 -3000”這樣的帶參效果
????????然后定義了一個(gè)自定義的錯(cuò)誤類型,這個(gè)可有可無
????????最后是鏈表的節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)由 一個(gè)命令字+一個(gè)功能函數(shù)+下個(gè)節(jié)點(diǎn)的地址? 組成
二、?具體實(shí)現(xiàn)(.C)
console.c
1.全局變量
????????定義了一個(gè)任務(wù)句柄,可有可無
????????定義了一個(gè)鏈表頭指針,用于查找控制整個(gè)鏈表
????????定義了一個(gè)串口接收數(shù)組,用于接收命令,空間可以給大點(diǎn)也可以給小一點(diǎn),因?yàn)槊畈⒉粫?huì)很長,但是考慮到誤操作的問題,太小了容易導(dǎo)致溢出
2.串口相關(guān)
????????第一個(gè)函數(shù)用于啟動(dòng)串口接收,使用DMA+串口空閑中斷的方式
????????第二函數(shù)是接收回調(diào)函數(shù),這個(gè)函數(shù)被放置在STM32的串口空閑事件中(
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
)其他單片機(jī)則放置在對(duì)應(yīng)的中斷觸發(fā)處,用于通知控制臺(tái)任務(wù)執(zhí)行以及使能下次接收
3.初始化控制臺(tái)
????????首先調(diào)用上面寫好的函數(shù)完成串口配置,然后為之前定義的頭指針分配一塊內(nèi)存,做一下簡單的空指針判斷,隨后完成初始化
4.命令注冊(cè)
????????該函數(shù)擁有兩個(gè)參數(shù),分別是指令和對(duì)應(yīng)函數(shù),首先定義一個(gè)“now”臨時(shí)節(jié)點(diǎn),讓他等于頭節(jié)點(diǎn),然后判斷他的 指向下個(gè)節(jié)點(diǎn)的 指針變量 是否為空,如果不為空則說明不是尾節(jié)點(diǎn),需要判斷一下當(dāng)前節(jié)點(diǎn)的指令和參數(shù)傳入的指令是否一致,如果一致則說明指令重復(fù),退出函數(shù)即可,如果指令不一樣,則讓now節(jié)點(diǎn)等于該變量指向的節(jié)點(diǎn),隨后進(jìn)入下一個(gè)循環(huán)判斷,直到找到尾節(jié)點(diǎn),找到尾節(jié)點(diǎn)后即可分配一塊節(jié)點(diǎn)內(nèi)存,并讓尾節(jié)點(diǎn)指向這個(gè)節(jié)點(diǎn),同時(shí)使用傳入?yún)?shù)為該節(jié)點(diǎn)變量進(jìn)行賦值,至此完成命令注冊(cè)
5. 命令卸載
????????使用該函數(shù)可以實(shí)現(xiàn)注銷一個(gè)指令的操作,但是我覺得沒什么用,因?yàn)樽?cè)了命令之后基本上也不會(huì)有什么注銷的需求,所以就沒寫(笑),如果需要的話可以自己實(shí)現(xiàn),無非就是比較到相同的指令,然后釋放這個(gè)節(jié)點(diǎn)的內(nèi)存,并讓上個(gè)節(jié)點(diǎn)指向下個(gè)節(jié)點(diǎn)
6.?命令調(diào)度
????????函數(shù)傳入的是終端輸入的命令字符串,首先用自制的字符串比較方法進(jìn)行指令的比較判斷,一個(gè)一個(gè)比較,直到發(fā)現(xiàn)不一樣的字符,此時(shí)看看節(jié)點(diǎn)的命令 是否 每個(gè)字符都比較完了,通過字符串結(jié)尾的\0判斷,如果當(dāng)前index的位置不是\0說明還沒比較結(jié)束就出現(xiàn)了不一樣的字符,指令不匹配進(jìn)入下一次循環(huán),如果是\0則說明匹配到了一個(gè)節(jié)點(diǎn),則可以調(diào)用該節(jié)點(diǎn)內(nèi)部的功能函數(shù),并且將上位機(jī)輸入內(nèi)容以參數(shù)傳進(jìn),繼續(xù)做參數(shù)解析(這樣子的好處是只關(guān)心輸入內(nèi)容的前面一部分是否與指令相同,而不關(guān)心其他的部分,可以方便的做指令參數(shù),例如注冊(cè)的指令為“open”,輸入的內(nèi)容為“open -1 -2”,即可匹配到open命令,并將整個(gè)字符串傳入到功能函數(shù)中,在函數(shù)內(nèi)解析出剩下的參數(shù))
7. 命令展示
????????一個(gè)功能函數(shù),用于打印出當(dāng)前已注冊(cè)的命令有哪些
????

終端介紹
? ? ? ??在開始下一節(jié)之前先介紹一個(gè)概念,終端的輸入方式和串口調(diào)試助手的輸入方式是不一樣的,在串口調(diào)試助手中會(huì)先輸完完整的信息,然后點(diǎn)擊發(fā)送,單片機(jī)一次性收到一整條消息,并且也能看到自己到底寫了什么,如下圖

???????終端則截然不同,首先終端的輸入是一個(gè)字一個(gè)字發(fā)送的,假設(shè)在終端中鍵盤按下了一個(gè)h,此時(shí)這個(gè)h就已經(jīng)發(fā)送給單片機(jī)了,并且終端上并不會(huì)顯示h,因?yàn)榻K端只管發(fā)送,要想看到自己寫了什么內(nèi)容,需要單片機(jī)接收到字符后再發(fā)送回終端,以此才能實(shí)現(xiàn)類似于Linux終端那樣的效果
????????我使用的終端是MobaXterm,不用付費(fèi)也能用,為什么要用終端不用串口助手呢,因?yàn)?/span>終端可以進(jìn)行顏色的解析,大部分串口助手只能顯示黑色的字,無法解析顏色

8. 控制臺(tái)任務(wù)
????????首先定義一個(gè)臨時(shí)數(shù)組,因?yàn)榻K端發(fā)送數(shù)據(jù)是一個(gè)一個(gè)發(fā)的,所以不能以一幀做為結(jié)束,在判斷回車結(jié)束之前需要存放在這個(gè)緩存區(qū)
????????將 查看已存在命令 的函數(shù)注冊(cè)到控制臺(tái)中,這樣就可以在控制臺(tái)查看已有命令了,將該函數(shù)與“l(fā)ist”命令綁定
????????使用任務(wù)等待,阻塞在這里,直到收到數(shù)據(jù)為止
????????收到消息后就將數(shù)據(jù)存入緩沖區(qū),同時(shí)記錄當(dāng)前數(shù)據(jù)的累計(jì)大小
????????判斷一下當(dāng)前數(shù)據(jù)的結(jié)尾是不是\r或者\(yùn)n,如果是的話則說明一幀結(jié)束,同時(shí)判斷一下數(shù)據(jù)大小,如果發(fā)現(xiàn)沒有數(shù)據(jù),只是在終端單純敲了個(gè)回車的話,則打印一個(gè)>>,看起來更有感覺,如果有數(shù)據(jù)則使用cs_fun_callback()函數(shù)進(jìn)行命令分發(fā)

????????終端里輸入命令的時(shí)候可能會(huì)輸錯(cuò),所以需要實(shí)現(xiàn)退格刪除功能,單片機(jī)檢測(cè)退格,如果發(fā)現(xiàn)輸入了退格,先把緩沖區(qū)的下標(biāo)記錄變量-2,為什么是-2不是-1,因?yàn)槭紫纫獪p掉當(dāng)前輸入的退格鍵,然后再減掉真正輸錯(cuò)的那個(gè)字,同時(shí)在控制臺(tái)先打印一個(gè)退格,讓光標(biāo)往前移動(dòng)一格,再打印一個(gè)空格,遮蓋掉打錯(cuò)的字,然后再發(fā)送一個(gè)退格,退回到即將輸入的位置
????????如果都不是上述的情況,則說明只是普通的輸入了一個(gè)字符,將這個(gè)字符通過終端打印出來,實(shí)現(xiàn)“回顯”的效果
????????當(dāng)然最重要的,要?jiǎng)?chuàng)建一個(gè)線程,讓他跑起來
9. 功能函數(shù)示例
????????該函數(shù)實(shí)現(xiàn)了控制 某個(gè)電機(jī) 以 指定速度 轉(zhuǎn)動(dòng) 多少圈,傳入?yún)?shù)由命令分發(fā)函數(shù)傳入,內(nèi)容就是輸入的串口內(nèi)容

用字符串切割函數(shù)即可取出所需的參數(shù),通過函數(shù)轉(zhuǎn)換為自己想要的類型后即可使用
結(jié)束
????????至此內(nèi)容結(jié)束,完成了完整的控制臺(tái)功能,可以自定義任意多的命令,并在單片機(jī)運(yùn)行的時(shí)候隨時(shí)使用,比如需要檢測(cè)任務(wù)剩余堆棧,以前需要一直打印信息,導(dǎo)致串口上有一大堆無用數(shù)據(jù),淹沒了真正重要的有價(jià)值的數(shù)據(jù),現(xiàn)在只需要將打印內(nèi)存信息的功能封裝成一個(gè)函數(shù),注冊(cè)一個(gè)命令,即可在適當(dāng)?shù)臅r(shí)候人為控制打印一次,方便簡潔
????????還可以注冊(cè)一些可能需要靈活更改、變化的功能,比如以前想要改某個(gè)參數(shù)或功能需要在代碼當(dāng)中更改,然后重新編譯下載,但是現(xiàn)在只需要提前做好功能函數(shù),即可通過控制臺(tái)隨時(shí)更改,極大的方便了測(cè)試
????????總之好處多多,該方案為本人原創(chuàng),歡迎提出意見?(?? ? ??)?
????????感謝閱讀(●'?'●)
