工作5年,野路子。由感而發,隨便寫點,混點積分。
工作之后已經很久沒有在論壇上活躍了,一方面沒有時間,另一方面有問題時直接聯系廠家技術支持了,很少在論壇里面請教東西。近來比較空閑,分享下自己寫的一些代碼方法。相信很多人初學 STM32是參考原子哥的例程的,為了方便大家理解,以下所寫就在原子哥基礎上加上一些自己的東西,當然很多也不定是我的東西,只不過掌握了而已。
就先說串口吧,這個大家想必大家都會。就直接開門見山了,說下下面兩點,串口發送和封 裝串口功能。對于串口發送,下面是原先的:
這種串口發送在波特率不是很高時不推薦這樣寫,就以波特率 9600 來算,發送 1 位就需要 104us,發送一字節(8 位數據+1 位起始位+1 位停止位)就需要 1ms,可以看到在發送的 時候 MCU 是處于一直等待的狀態,如果發送 100 個字節,就在這里等待 100ms 以上,在項 目中使用顯然不合適。
實際在用的過程中,是建立一個狀態標志,用來表示串口發送是忙還是空閑,當我們發送置 為忙,發送完成置為空閑。利用串口發送中斷避免原子例程中的while 等待。如下面:
對于沒有使用過串口發送中斷的,可以自行搜索,這里不做過多解釋,總之這種串口發送不 會讓主程序一直等待硬件標志,當然這種發送方式需要上層應用做出判斷,就是在當前數據 沒有發送完前,不能發送新的數據。原先的寫法可以連續發送很多條字符串,現在需要寫個
狀態機,判斷當前串口是否忙,然后才能進行下一步操作。例如有 10 個數組,把 10 個數組 分別通過串口發送出去,每個數組發送完需要間隔 10ms 才能發送下一個數組。要實現這樣 的功能,當然可以用一個 for 循環搞定這個操作,但是如果在不影響實時性的基礎上實現, 這個會在后面的程序框架中講到。
串口發送就到此為止,接下來講下封裝串口功能。對于一個多人維護的項目來說,并不需要 每個人都把全部代碼或者別人寫的代碼都一一摸清楚,一個串口,我需要的就是很簡單的功 能,打開串口,串口來數據里怎么辦,串口發送完數據了怎么辦。就像下面這種:
這是一個函數申明,前三個參數是需要打開的串口號、波特率、校驗,第四和第五個參數是 函數指針,函數指針就是一個指針,只不過指向的是函數而已。這里來說明一下函數指針的 用法: 可以看到,函數指針可以指向一個同類型的函數,并且可以運行這個函數指針指向的函數。 知道這些接下來就可以亮出代碼解釋函數申明里面的兩個回調函數了。
先從底層說起,首先申明一個結構體類型,
然后定義這個結構體變量,這個結構體分別對應 5 個串口的基本信息:
以串口 1 的中斷函數為例,通過紅色框中可以看出,每接收一個串口數據,會調用對應結構 體變量的 rx_call_back 函數指針。每發送完串口數據會調用結構體變量的 tx_call_back 函數 指針。
可以看到,這兩個函數指針指向的函數已經被執行,就差一點了,這個函數指針指向的是哪 個函數?好了回到串口初始化函數,如下:
可以看到所指向的函數是通過 uart_open 傳遞過來的,具體指向哪個函數交于上層調用 uart_open 的來指定,底層的工作先到此結束(為了使代碼好看,把串口 2、3、4、5 及引腳 配置都省略了,寫這篇的意義不在意代碼,而是思路)。
底層再補充一下串口發送及狀態獲取的代碼吧:
這下底層的工作真的到此結束了。再從上層角度看下怎么使用這個函數,比如通過串口 1 和 
NB 模組進行通信,下面就是一個打開串口的函數:

下面是串口回調函數:
可以看到上層程序設計相對來說已經不再過多涉及 STM32 本身的操作了,這種回調函數的 用法,使得程序分層設計,思路上更加清晰,方便維護。
再說定時器吧,也是開門見山,直接說自己的兩點,封裝定時器和如果使用定時器。 先說如何封裝定時器吧,其實也就是回調函數,和上面封裝串口一樣。這里就直接曬代碼了, 下面是底層代碼:
下面是上層代碼,和封裝串口類似,上層只要知道打開一個定時器,多長時間進入自己定義 的定時器中斷就可以了。
接下來就是如何使用定時器了,我這定時器可能和你想的不一樣,這里主要介紹一下思路。 前面都是先從底層說起,這里換個角度先從上層說起,對應上層的設計人員,該人員需要的
僅僅是定義一個變量,然后注冊到定時器里。如下面:
當上層人員調用 time_cnt_reg 函數,實際就已經注冊一個定時器了,比如 rx_over_time 就會 每 1ms 增加 1,inq_reg_over_time 每 1s 增加 1。上層人員知道這么用就可以了,當然也許 你會好奇,對這個變量進行查找,并沒有對該變量的操作,怎么就增加了呢。
從接下來開始,所說的操作也不算是底層操作了,應算是整個程序設計中的框架功能。以下 就從框架功能角度說了,主要就是 time_cnt_reg 是怎么一回事。 其實也很簡單,定義一個鏈表,鏈表中就一個時間單位,是 ms 還是 s,然后就是一個指針, 指向的是待注冊的變量。
每次調用 time_cnt_reg 函數,就會向鏈表中增加一個成員,成員中的 cnt_ptr 指向 time_cnt_reg中帶入參數地址。
下面就是對鏈表中的成員進行自加 1 操作,
上面這兩個函數在哪里使用的呢,回到封裝定時器操作,里面的回調函數做了什么?
這下應該清楚了注冊定時器是怎么一回事了。我這里用的注冊定時器用的鏈表方式,也可以 改成結構體數組。以前我也用過結構體數組,結果有次注冊的定時器超過定義的上限,結果 排查了半天才找到原因才改的使用鏈表。
好了,再回到上層應用,注冊好定時器后,應用起來就和正常使用的一樣就可以了,可能你 會說既然用起來都一樣,為何你這還要多此一舉,這個變量完全可以在定時器中斷里面直接 使用 rx_over_time++搞定的事么。這個么仁者見仁智者見智,有興趣的可以接著往下看。
再說程序框架吧,很多人喜歡上一些實時操作系統,在我看來其實沒有必要,那些實時操作 系統可以當做學習 LINUX 的過渡,但使用起來沒覺得哪里特別的。自己搭一個抽象系統+狀 態機就可以搞定的事情,程序設計更加健壯、可控、高效。這里的抽象系統就算是我所說的 程序框架吧。
先說任務處理吧,通過 app_reg 注冊一個任務,思路和前面使用定時器差不多,每次 app_reg 執行都會向鏈表中添加一個節點,然后在定時器回調函數中對該APP 時間計數加 1 操作。 為了節約篇幅這里就不貼出這部分代碼了。
主函數的代碼很短,主要就是 app_init 和 app_manage,查看源碼發現僅僅有 2 處區別, app_init 函數不需要對應的計數器到達設計值就可以,但是app_manage 需要計數器到達設 定值才執行。其次 app_init 執行時帶入的參數為 0,app_manage 執行時帶入的參數為 1。
下面以內部看門狗的示例展示一下用法:
可以從代碼中看出,data 為 0 時即初始化看門狗,為 1 時即每 50ms 執行一次,我個人覺得 這么寫的話代碼緊湊一些,方便查看。看過很多裸跑的程序,建立幾個時間標志,10ms 到 了干什么什么事,1s 到了干什么什么事,結果同樣是一個事情,這里一行代碼,那里一行代 碼,時間長了就不知道還有哪里會有這些代碼。
再以一個例子引入下一個程序功能吧,485 總線收發數據,當需要發送 485 數據時,先將控 制收發引腳置為發送,然后將串口數據發送出去,然后稍微加些延時,再然后將控制引腳置 為接收。我想很多人操作這個時應該是讓 MCU 硬等待在這一塊。看了我上面寫的串口代碼, 可以這樣寫,當發送時,先將 485 置為發送,然后發送數據,在串口發送完成回調函數中將 485 再置為接收。 然后實際測試卻不是這樣,發送串口數據完成然后在回調函數中立刻置為接收,會造成 485 最后一個字節數據發送不完整,在回調函數中加入延時再置為接收就沒有問題,但是這里的 延時,實際上就是在串口發送中斷函數執行的,很明顯不合理。如何解決這個問題,程序框 架中又加入一項功能:多長時間之后執行某個函數,且只執行一遍。具體設計思路不再貼出 來了,實現這一功能和前面的都差不多,這里只說下怎么應用的,con_485_recv 是一個控制 485 為接收的函數,在串口發送完成回調函數中執行 fun_once_ms_late_reg(10, con_485_recv);
即 10ms 后執行 con_485_recv。為了避免 485 連續發送幾條數據時,之前的 con_485_recv 到 時間了執行,在發送數據時除了置為發送,還要調用fun_once_ms_late_unreg(con_485_recv), 即取消還有多長時間后執行的 con_485_recv,避免時序錯誤。
再說下狀態機吧,以操作 NB-IOT 工作在主動上報類為例,相信很多人看過原子的操作 AT 指令的函數,如下:
然后就是調用這個函數一個個執行 AT 命令,這種寫法在例程里作為實驗是可以的,但是實 際應用不該這么寫,影響系統的實時性,也難以維護。下面是操作NB 模組工作的流程圖, 按照如下流程進行程序設計。

下面是對 NB 狀態的相關定義、變量申明及初始化為上電流程
下面就是主要的處理了,我們讓 nbiot_proc 運行的時基為 50ms,AT 命令發送處理中,不同 AT 指令響應的時間不同,有些需要幾百毫秒,有些需要幾秒,就通過 nb_work.delay 實現發 送處理的延時處理,這里的延時并不影響系統的實時性。另外對比發送處理和接收處理,可
以發送不同的流程下每個流程都公用一個處理函數,只不過帶入的參數不同,我覺得這樣寫 便于閱讀而已。
以上電初始化代碼參考如下,可以看到,首先斷電并等待 2s,然后上電等待 5s,然后置為 參數初始化流程,在這 5s 期間,如果收到模塊發出的對應信息,提前結束等待時間,并且 轉到下一個流程。其實通過這一個流程就已經差不多表述了我所想表達的狀態機寫法,這樣 的寫法有兩個好處,一是實時性保持良好,二是通過 state 和 process 就可以知道當前模塊 工作在什么狀態哪個流程,做到“可控”。

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

下面是發送消息的處理,其實就是申請一個消息隊列 上層只管對消息隊列進行處理,而消息隊列的處理,是通過下面的這個函數進行處理。其實 這個并不是隊列,只是一種延伸的用法,不過習慣了這么叫。
再說下一些微小功能吧,主要是方便而已。首先是調試串口,這個可以用來和 MCU 進行一 些 SHELL 指令交互,比如板上有個 NANDFLASH 或者 SD 卡,搭載文件系統,想要看里面文 件時,總不能每次都把卡拔出來看吧,這時可以通過調試串口進行命令交互,就像下面一樣, 當然這部分程序要寫:
當然還可以有其他功能,比如查看任何一個外設的狀態,查看某個串口交互的全部報文等,
就看應用者如何賦予功能。總不能想看下串口收發報文,用個 USB 轉 TTL 焊在對應的引腳 上吧,太不方便了。
再說下LOG吧,批量的東西,經常有很多難以復現的問題,這些問題很多是在特定的情況 下觸發的軟件 BUG,這種情況下,如果有 LOG 功能,就方便分析問題了,但是如果沒有那 也只能靠猜了。導出 LOG 也可以通過調試串口來實現,總之就是為了方便。
再說下低功耗吧,其實這一塊感覺也沒啥寫的,設計思路是這樣的,對于任何一個模塊,比 如 NB 模塊和對 NB 消息隊列的處理,每個都向睡眠機制中注冊一個變量,NB 底層狀態機 處理只要不是空閑,對應變量都為真,NB 消息隊列只要有未處理的數據,對應變量也為真。 睡眠管理在 while(1)里,檢查所有注冊的變量,當所有的變量都為假時,調用注冊的回調函 數并進入睡眠,然后多長時間喚醒一次再通過另一個回調函數進行某項處理。下面是應用在 華大一款低功耗芯片的 main 函數:
對應的低功耗還有時鐘切換,也是向時鐘管理機制中注冊一個變量,比如某段時間只有幾個 led 亮著,那就用 32768Hz 的時鐘做主頻,如果有用 IIC 的,就用 4M 做主頻等等,while(1) 都是對注冊的變量進行判斷管理。 其實之前也沒有搞過太多低功耗的東西,只是不想破壞原有的各種外設的代碼及風格,便這 樣處理了。下面是睡眠管理處理,涉及到對底層寄存器的處理和芯片的工作模式,看過一段 時間現在忘了。時間長了我都不知道寫的啥,只知道上層怎么使用的了,但這我覺得就夠了。
先寫到這邊吧,看不明白沒關系,因為我覺得自己寫的都不知道寫的啥,文筆太差,隨便寫寫。
以上的pdf格式文檔51黑下載地址(內含清晰圖):
嵌入式軟件隨筆.pdf
(1.37 MB, 下載次數: 36)
2020-3-30 11:07 上傳
點擊文件名下載附件
嵌入式軟件隨筆 下載積分: 黑幣 -5
|