標題: 關于STM32嵌入式程序調試的體會 [打印本頁]

作者: hubj627    時間: 2020-3-30 11:21
標題: 關于STM32嵌入式程序調試的體會
工作5年,野路子。由感而發(fā),隨便寫點,混點積分。
工作之后已經(jīng)很久沒有在論壇上活躍了,一方面沒有時間,另一方面有問題時直接聯(lián)系廠家技術支持了,很少在論壇里面請教東西。近來比較空閑,分享下自己寫的一些代碼方法。相信很多人初學 STM32是參考原子哥的例程的,為了方便大家理解,以下所寫就在原子哥基礎上加上一些自己的東西,當然很多也不定是我的東西,只不過掌握了而已。

就先,這個大家想必大家都會。就直接開門見山了,說下下面兩點,串口發(fā)送和封 裝串口功能。對于串口發(fā)送,下面是原先的:

這種串口發(fā)送在波特率不是很高時不推薦這樣寫,就以波特率 9600 來算,發(fā)送 1 位就需要
104us,發(fā)送一字節(jié)(8 位數(shù)據(jù)+1 位起始位+1 位停止位)就需要 1ms,可以看到在發(fā)送的 時候 MCU 是處于一直等待的狀態(tài),如果發(fā)送 100 個字節(jié),就在這里等待 100ms 以上,在項 目中使用顯然不合適。

實際在用的過程中,是建立一個狀態(tài)標志,用來表示串口發(fā)送是忙還是空閑,當我們發(fā)送置 為忙,發(fā)送完成置為空閑。利用串口發(fā)送中斷避免原子例程中的while 等待。如下面:


對于沒有使用過串口發(fā)送中斷的,可以自行搜索,這里不做過多解釋,總之這種串口發(fā)送不 會讓主程序一直等待硬件標志,當然這種發(fā)送方式需要上層應用做出判斷,就是在當前數(shù)據(jù) 沒有發(fā)送完前,不能發(fā)送新的數(shù)據(jù)。原先的寫法可以連續(xù)發(fā)送很多條字符串,現(xiàn)在需要寫個

狀態(tài)機,判斷當前串口是否忙,然后才能進行下一步操作。例如有 10 個數(shù)組,把 10 個數(shù)組 分別通過串口發(fā)送出去,每個數(shù)組發(fā)送完需要間隔 10ms 才能發(fā)送下一個數(shù)組。要實現(xiàn)這樣
的功能,當然可以用一個 for 循環(huán)搞定這個操作,但是如果在不影響實時性的基礎上實現(xiàn), 這個會在后面的程序框架中講到。

串口發(fā)送就到此為止,接下來講下封裝串口功能。對于一個多人維護的項目來說,并不需要 每個人都把全部代碼或者別人寫的代碼都一一摸清楚,一個串口,我需要的就是很簡單的功 能,打開串口,串口來數(shù)據(jù)里怎么辦,串口發(fā)送完數(shù)據(jù)了怎么辦。就像下面這種:

這是一個函數(shù)申明,前三個參數(shù)是需要打開的串口號、波特率、校驗,第四和第五個參數(shù)是 函數(shù)指針,函數(shù)指針就是一個指針,只不過指向的是函數(shù)而已。這里來說明一下函數(shù)指針的 用法:
可以看到,函數(shù)指針可以指向一個同類型的函數(shù),并且可以運行這個函數(shù)指針指向的函數(shù)。 知道這些接下來就可以亮出代碼解釋函數(shù)申明里面的兩個回調函數(shù)了。

先從底層說起,首先申明一個結構體類型,

然后定義這個結構體變量,這個結構體分別對應 5 個串口的基本信息:

以串口 1 的中斷函數(shù)為例,通過紅色框中可以看出,每接收一個串口數(shù)據(jù),會調用對應結構 體變量的 rx_call_back 函數(shù)指針。每發(fā)送完串口數(shù)據(jù)會調用結構體變量的 tx_call_back 函數(shù)
指針。

可以看到,這兩個函數(shù)指針指向的函數(shù)已經(jīng)被執(zhí)行,就差一點了,這個函數(shù)指針指向的是哪 個函數(shù)?好了回到串口初始化函數(shù),如下:


可以看到所指向的函數(shù)是通過 uart_open 傳遞過來的,具體指向哪個函數(shù)交于上層調用
uart_open 的來指定,底層的工作先到此結束(為了使代碼好看,把串口 2、3、4、5 及引腳 配置都省略了,寫這篇的意義不在意代碼,而是思路)。

底層再補充一下串口發(fā)送及狀態(tài)獲取的代碼吧:


這下底層的工作真的到此結束了。再從上層角度看下怎么使用這個函數(shù),比如通過串口 1 和

NB 模組進行通信,下面就是一個打開串口的函數(shù):


下面是串口回調函數(shù):

可以看到上層程序設計相對來說已經(jīng)不再過多涉及 STM32 本身的操作了,這種回調函數(shù)的 用法,使得程序分層設計,思路上更加清晰,方便維護。

再說,也是開門見山,直接說自己的兩點,封裝定時器和如果使用定時器。 先說如何封裝定時器吧,其實也就是回調函數(shù),和上面封裝串口一樣。這里就直接曬代碼了,
下面是底層代碼:



下面是上層代碼,和封裝串口類似,上層只要知道打開一個定時器,多長時間進入自己定義 的定時器中斷就可以了。

接下來就是如何使用定時器了,我這定時器可能和你想的不一樣,這里主要介紹一下思路。 前面都是先從底層說起,這里換個角度先從上層說起,對應上層的設計人員,該人員需要的

僅僅是定義一個變量,然后注冊到定時器里。如下面:


當上層人員調用 time_cnt_reg 函數(shù),實際就已經(jīng)注冊一個定時器了,比如 rx_over_time 就會 每 1ms 增加 1,inq_reg_over_time 每 1s 增加 1。上層人員知道這么用就可以了,當然也許 你會好奇,對這個變量進行查找,并沒有對該變量的操作,怎么就增加了呢。

從接下來開始,所說的操作也不算是底層操作了,應算是整個程序設計中的框架功能。以下 就從框架功能角度說了,主要就是 time_cnt_reg 是怎么一回事。 其實也很簡單,定義一個鏈表,鏈表中就一個時間單位,是 ms 還是 s,然后就是一個指針, 指向的是待注冊的變量。



每次調用 time_cnt_reg 函數(shù),就會向鏈表中增加一個成員,成員中的 cnt_ptr 指向 time_cnt_reg中帶入?yún)?shù)地址。

下面就是對鏈表中的成員進行自加 1 操作,

上面這兩個函數(shù)在哪里使用的呢,回到封裝定時器操作,里面的回調函數(shù)做了什么?

這下應該清楚了注冊定時器是怎么一回事了。我這里用的注冊定時器用的鏈表方式,也可以 改成結構體數(shù)組。以前我也用過結構體數(shù)組,結果有次注冊的定時器超過定義的上限,結果 排查了半天才找到原因才改的使用鏈表。

好了,再回到上層應用,注冊好定時器后,應用起來就和正常使用的一樣就可以了,可能你 會說既然用起來都一樣,為何你這還要多此一舉,這個變量完全可以在定時器中斷里面直接 使用 rx_over_time++搞定的事么。這個么仁者見仁智者見智,有興趣的可以接著往下看。


再說,很多人喜歡上一些實時操作系統(tǒng),在我看來其實沒有必要,那些實時操作 系統(tǒng)可以當做學習 LINUX 的過渡,但使用起來沒覺得哪里特別的。自己搭一個抽象系統(tǒng)+狀
態(tài)機就可以搞定的事情,程序設計更加健壯、可控、高效。這里的抽象系統(tǒng)就算是我所說的 程序框架吧。

先說,通過 app_reg 注冊一個任務,思路和前面使用定時器差不多,每次 app_reg 執(zhí)行都會向鏈表中添加一個節(jié)點,然后在定時器回調函數(shù)中對該APP 時間計數(shù)加 1 操作。 為了節(jié)約篇幅這里就不貼出這部分代碼了。

主函數(shù)的代碼很短,主要就是 app_init 和 app_manage,查看源碼發(fā)現(xiàn)僅僅有 2 處區(qū)別, app_init 函數(shù)不需要對應的計數(shù)器到達設計值就可以,但是app_manage 需要計數(shù)器到達設 定值才執(zhí)行。其次 app_init 執(zhí)行時帶入的參數(shù)為 0,app_manage 執(zhí)行時帶入的參數(shù)為 1。



下面以內部看門狗的示例展示一下用法:


可以從代碼中看出,data 為 0 時即初始化看門狗,為 1 時即每 50ms 執(zhí)行一次,我個人覺得 這么寫的話代碼緊湊一些,方便查看?催^很多裸跑的程序,建立幾個時間標志,10ms 到 了干什么什么事,1s 到了干什么什么事,結果同樣是一個事情,這里一行代碼,那里一行代 碼,時間長了就不知道還有哪里會有這些代碼。

再以一個例子引入下一個程序功能吧,485 總線收發(fā)數(shù)據(jù),當需要發(fā)送 485 數(shù)據(jù)時,先將控 制收發(fā)引腳置為發(fā)送,然后將串口數(shù)據(jù)發(fā)送出去,然后稍微加些延時,再然后將控制引腳置 為接收。我想很多人操作這個時應該是讓 MCU 硬等待在這一塊?戳宋疑厦鎸懙拇诖a, 可以這樣寫,當發(fā)送時,先將 485 置為發(fā)送,然后發(fā)送數(shù)據(jù),在串口發(fā)送完成回調函數(shù)中將
485 再置為接收。 然后實際測試卻不是這樣,發(fā)送串口數(shù)據(jù)完成然后在回調函數(shù)中立刻置為接收,會造成 485 最后一個字節(jié)數(shù)據(jù)發(fā)送不完整,在回調函數(shù)中加入延時再置為接收就沒有問題,但是這里的 延時,實際上就是在串口發(fā)送中斷函數(shù)執(zhí)行的,很明顯不合理。如何解決這個問題,程序框 架中又加入一項功能:長時執(zhí)數(shù),執(zhí)。具體設計思路不再貼出 來了,實現(xiàn)這一功能和前面的都差不多,這里只說下怎么應用的,con_485_recv 是一個控制
485 為接收的函數(shù),在串口發(fā)送完成回調函數(shù)中執(zhí)行 fun_once_ms_late_reg(10, con_485_recv);

即 10ms 后執(zhí)行 con_485_recv。為了避免 485 連續(xù)發(fā)送幾條數(shù)據(jù)時,之前的 con_485_recv 到 時間了執(zhí)行,在發(fā)送數(shù)據(jù)時除了置為發(fā)送,還要調用fun_once_ms_late_unreg(con_485_recv),
即取消還有多長時間后執(zhí)行的 con_485_recv,避免時序錯誤。

再說態(tài),以操作 NB-IOT 工作在主動上報類為例,相信很多人看過原子的操作 AT
指令的函數(shù),如下:


然后就是調用這個函數(shù)一個個執(zhí)行 AT 命令,這種寫法在例程里作為實驗是可以的,但是實 際應用不該這么寫,影響系統(tǒng)的實時性,也難以維護。下面是操作NB 模組工作的流程圖, 按照如下流程進行程序設計。



下面是對 NB 狀態(tài)的相關定義、變量申明及初始化為上電流程

下面就是主要的處理了,我們讓 nbiot_proc 運行的時基為 50ms,AT 命令發(fā)送處理中,不同 AT 指令響應的時間不同,有些需要幾百毫秒,有些需要幾秒,就通過 nb_work.delay 實現(xiàn)發(fā) 送處理的延時處理,這里的延時并不影響系統(tǒng)的實時性。另外對比發(fā)送處理和接收處理,可

以發(fā)送不同的流程下每個流程都公用一個處理函數(shù),只不過帶入的參數(shù)不同,我覺得這樣寫 便于閱讀而已。



以上電初始化代碼參考如下,可以看到,首先斷電并等待 2s,然后上電等待 5s,然后置為
參數(shù)初始化流程,在這 5s 期間,如果收到模塊發(fā)出的對應信息,提前結束等待時間,并且 轉到下一個流程。其實通過這一個流程就已經(jīng)差不多表述了我所想表達的狀態(tài)機寫法,這樣 的寫法有兩個好處,一是實時性保持良好,二是通過 state 和 process 就可以知道當前模塊 工作在什么狀態(tài)哪個流程,做到“可控”。


下面是對接收到的消息處理,主要就是把 NB 收到的消息放入消息隊列里(稍微有些刪減):


下面是發(fā)送消息的處理,其實就是申請一個消息隊列 上層只管對消息隊列進行處理,而消息隊列的處理,是通過下面的這個函數(shù)進行處理。其實
這個并不是隊列,只是一種延伸的用法,不過習慣了這么叫。


再說下一些微小功能吧,主要是方便而已。調試,這個可以用來和 MCU 進行一
些 SHELL 指令交互,比如板上有個 NANDFLASH 或者 SD 卡,搭載文件系統(tǒng),想要看里面文 件時,總不能每次都把卡拔出來看吧,這時可以通過調試串口進行命令交互,就像下面一樣, 當然這部分程序要寫:

當然還可以有其他功能,比如查看任何一個外設的狀態(tài),查看某個串口交互的全部報文等,

就看應用者如何賦予功能。總不能想看下串口收發(fā)報文,用個 USB 轉 TTL 焊在對應的引腳 上吧,太不方便了。



LOG,批量的東西,經(jīng)常有很多難以復現(xiàn)的問題,這些問題很多是在特定的情況 下觸發(fā)的軟件 BUG,這種情況下,如果有 LOG 功能,就方便分析問題了,但是如果沒有那 也只能靠猜了。導出 LOG 也可以通過調試串口來實現(xiàn),總之就是為了方便。


再說,其實這一塊感覺也沒啥寫的,設計思路是這樣的,對于任何一個模塊,比 如 NB 模塊和對 NB 消息隊列的處理,每個都向睡眠機制中注冊一個變量,NB 底層狀態(tài)機 處理只要不是空閑,對應變量都為真,NB 消息隊列只要有未處理的數(shù)據(jù),對應變量也為真。 睡眠管理在 while(1)里,檢查所有注冊的變量,當所有的變量都為假時,調用注冊的回調函 數(shù)并進入睡眠,然后多長時間喚醒一次再通過另一個回調函數(shù)進行某項處理。下面是應用在 華大一款低功耗芯片的 main 函數(shù):


對應的低功耗還有時鐘切換,也是向時鐘管理機制中注冊一個變量,比如某段時間只有幾個
led 亮著,那就用 32768Hz 的時鐘做主頻,如果有用 IIC 的,就用 4M 做主頻等等,while(1) 都是對注冊的變量進行判斷管理。 其實之前也沒有搞過太多低功耗的東西,只是不想破壞原有的各種外設的代碼及風格,便這 樣處理了。下面是睡眠管理處理,涉及到對底層寄存器的處理和芯片的工作模式,看過一段 時間現(xiàn)在忘了。時間長了我都不知道寫的啥,只知道上層怎么使用的了,但這我覺得就夠了。



先寫到這邊吧,看不明白沒關系,因為我覺得自己寫的都不知道寫的啥,文筆太差,隨便寫寫。

以上的pdf格式文檔51黑下載地址(內含清晰圖):
嵌入式軟件隨筆.pdf (1.37 MB, 下載次數(shù): 36)

作者: hubj627    時間: 2020-4-1 08:46
已上傳的文檔好像不能再次編輯,可以通過GIT獲取最新WORD版文檔,不定期更新。有任何疑問此貼必回。
https://gitee.com/hubaojin/ARM_DEBUG_STUDY.git
作者: hghfghfhgfh    時間: 2020-4-4 21:07
感謝分享
作者: hghfghfhgfh    時間: 2020-4-4 21:07
感謝分享

作者: 瘋子本人    時間: 2020-4-4 22:13
受教頗多,謝謝分享經(jīng)驗
作者: flycat    時間: 2020-4-4 22:21
支持樓主
作者: flycat    時間: 2020-4-4 22:22
受教頗多,謝謝分享經(jīng)驗
作者: qianchan    時間: 2020-4-5 17:48
感謝分享啊~學習了~

作者: lyseg    時間: 2020-4-11 10:15

感謝分享!!
作者: ywjianghu    時間: 2020-4-12 19:59
調理清楚,表達清晰準確!樓主加油!
作者: gtzlz    時間: 2020-4-25 01:41
學習了,謝謝樓主!!
作者: ballecho    時間: 2020-4-25 14:10
學習了,謝謝分享
作者: IdeaMing    時間: 2020-4-25 17:15
多謝分享,很有參考意義
作者: 莫燁    時間: 2020-4-26 11:14
干貨滿滿哦,謝謝!
作者: m18923252991    時間: 2020-4-26 16:51
很有份量的分享,受教了
作者: 風心落寞    時間: 2020-4-26 23:37
stm32正點原子和野火配合著看學習最好
作者: yu175921426    時間: 2020-4-27 16:04
厲害! 感謝分享
作者: 99312312    時間: 2020-5-1 20:29
謝謝詳細講解




歡迎光臨 (http://www.torrancerestoration.com/bbs/) Powered by Discuz! X3.1