天天看點

【HAL庫】序列槽通信HAL庫入門之序列槽通信

HAL庫入門之序列槽通信

背景

首先因為某比賽需要,選用了STM32L4R5ZI的闆子(不得不吐槽一下,不像STM32F1或者F4之類的主流闆子,資料真的很難找),查全網發現官方提供的包括某寶提供的等等都是基于HAL庫的代碼,是以,不得不學習一下HAL庫的使用。

1. STM32CubeMX(代碼生成器)

STM32CubeMX 是 ST 意法半導體近幾年來大力推薦的STM32 晶片圖形化配置工具, 允許使用者使用圖形化向導生成C 初始化代碼,可以大大減輕開發工作,時間和費用。(可以在官網下載下傳)

簡單地說就是,以圖形化的方式進行系統時鐘、序列槽、GPIO、中斷等初始化配置,之後CubeMX會一鍵生成基于HAL庫程式設計方式的基礎代碼,之後可以通過Keil及IAR軟體進行後續程式設計。

1.1 基于board建立工程

CubeMX建立工程有兩種方式,基于MCU/MPU以及基于board兩種,基于MCU/MPU方式建立的話,包括系統時鐘、LED、GPIO等方面都要全部自定義配置,對于剛入門的新手來說,個人感覺是比較困難的,是以選擇了基于board建立工程的方式:

打開CubeMX後選擇左上角File——>New Project,切換左上角建立工程方式為基于board的方式,下滑進度條選擇NUCLEO-L4R5ZI後點選右上角Start Project,如下圖所示:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

打開後,界面如下圖所示:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

導航欄中選擇Project Manager,自定義項目名稱和工程存放目錄,之後将Toolchain/IDE欄改為MDK-ARM V5,因為我後續将使用Keil進行後續代碼開發,如下圖所示:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

之後因為序列槽通信需要用到中斷,在導航欄中Pinout & Configuration下的System view下選擇LPUART1,之後在出來的界面中選擇NVIC Settings,勾選Enable打開序列槽全局中斷,之後點選右上角GENERATE CODE生成代碼:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

1.2 基于Keil程式設計

在使用STM32CubeMX生成初始代碼後,使用Keil打開工程(在打開工程之前,需要下載下傳闆子對應的pack包,可網上自行下載下傳,部落客本人用的2.1.0版本,沒什麼别的原因,因為我隻下載下傳的到這個版本的,輕按兩下即可安裝)

【HAL庫】序列槽通信HAL庫入門之序列槽通信

打開後,界面如下圖所示,找到main.c檔案中的main函數入口,可以對系統有個較為直覺的認識:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

如圖,main函數中依次進行了HAL庫的初始化、系統時鐘的初始化、GPIO的初始化、LPUART1序列槽序列槽初始化、UART3序列槽初始化、USB初始化。

此外引用一下另外一篇算是我的啟蒙部落格:https://blog.csdn.net/weixin_43186792/article/details/88759321,其中有一段正點原子的解釋如下:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

裡面提到了“MSP”函數和Init入口參數概念,結合打開後的keil工程後,可以了解為這個Core檔案夾下,“it.c”檔案主要涉及系統中斷部分,“msp.c”部分即對應正點原子解釋中的“MSP”部分,“main.c”中則是對應的“Init”部分,簡單來說就是,序列槽及外設的通用部分的初始化内容,在移植代碼時不需要進行更改的通用配置如波特率、奇偶校驗、停止位等部分在main.c中的Init函數中實作,而序列槽等根據平台闆子不同需要進行修改的部分,在msp.c中進行修改和實作,此處給我最大的感受就是HAL庫更像是在标準庫的基礎上追加了“解耦”操作。

【HAL庫】序列槽通信HAL庫入門之序列槽通信

1.3 序列槽通信

之前我們勾選了LPUART1的中斷,可以在it.c檔案中檢視(如果沒有在CubeMX中勾選使能該序列槽,這裡就看不到這個函數):

【HAL庫】序列槽通信HAL庫入門之序列槽通信

可以看見函數中定義了一個通用的中斷,hlpuart1作為入口參數,點進去之後如下:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

可以看到是關于中斷處理的一些函數處理,包括序列槽通信時出現問題時傳回ErrorCode等,相當于HAL庫将序列槽中斷處理函數幫我們一鍵寫好了,還是比較友善的,那麼,如果我們想要在序列槽收到資料時進行一些自定義的操作時怎麼辦呢?這裡提供一下網上常用的方法:調用“void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)”函數進行回調,這個函數原來在stm32l4xx_hal_uart.c中定義了,如下圖所示:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

可見,是個“__weak”弱類型函數,意味着可以在其他地方進行重寫,重寫之後,執行重寫之後的函數體内容,是以我們可以直接在main.c檔案中進行重寫。

簡單步驟如下:

①在main.c檔案中的序列槽初始化函數MX_LPUART1_UART_Init最後加一句中斷注冊,這裡的RxBuffer是我們自己定義的數組,用來接收序列槽接收到的資料:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

②在main.c檔案中重寫callback函數,這裡Transmit中的1表示發送的位元組長度為1,即無論實際的RxBuffer有多大,隻發送RxBuffer[0]的内容,100表示在100ms(應該是毫秒,但是我沒求證過)之内發送完資料,Recevice_IT中的1表示每接收到幾個位元組的資料進一次中斷,即調用一次callback函數(如果Receive這裡寫2,Transmit寫1,序列槽發送OK,則每發送一次OK,序列槽傳回一個O,這裡可以自己多測試一下,加深一下了解,需要注意的是,最開始的是由序列槽初始化函數下面的終端注冊決定的,進中斷處理函數之後,會自動清空标志位,如果callback函數中不重新注冊則隻能第一次接收到資料,後面的資料接收不到):

【HAL庫】序列槽通信HAL庫入門之序列槽通信

③将闆子通過USB接到電腦端口上,闆子帶有一個模拟序列槽,不需要通過USB轉TTL序列槽的CH340驅動再配置序列槽,燒寫程式前,需要在keil魔術棒中的debug裡添加flash燒寫算法,部落客用的闆子對應的如下:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

④燒寫程式之後,打開序列槽調試助手(XCOM或者SSCOM都可以),實作的功能就是,序列槽助手向單片機發送資料後,把收到的位元組原樣發送出去,這部分内容,網上的教程很多,如果在實作過程中出現問題,可以查詢其他部落客的文章。

1.4 接收中斷RxBuffer

1.4.1 序列槽收發資料方式

HAL庫序列槽收發資料有幾種不同方式,分别用到不同的函數,這部分在stm32l4xx_hal_uart.c檔案中有寫:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

大概意思翻譯後如下:

①阻塞模式:以輪詢方式進行通信,所有資料處理的HAL狀态在完成資料傳輸後由相同的函數傳回;
②非阻塞模式:通過中斷或DMA方式進行通信,由這些API傳回HAL狀态。當使用中斷模式或DMA模式下的DMA IRQ時,資料處理結束後結果将通過專用UART IRQ給出。HAL_UART_TxCpltCallback(), HAL_UART_RxCpltCallback()使用者回調将分别在發送或接收過程的結尾執行,當檢測到通信錯誤時,将執行HAL_UART_ErrorCallback()使用者回調。
	阻塞模式下的API:
	(+) HAL_UART_Transmit()
	(+) HAL_UART_Receive()
	非阻塞模式下中斷模式API:
	(+) HAL_UART_Transmit_IT()
	(+) HAL_UART_Receive_IT()
	(+) HAL_UART_IRQHandler()
	非阻塞模式下DMA模式API:
	(+) HAL_UART_Transmit_DMA()
	(+) HAL_UART_Receive_DMA()
	(+) HAL_UART_DMAPause()
	(+) HAL_UART_DMAResume()
	(+) HAL_UART_DMAStop()
           

按照全網常用序列槽收發配置模式,即本文1.3章節所述方式,主要采用中斷方式進行資料接收,轉發收到的資料的時候用的是阻塞模式發送,大概是因為不需要在發送資料的過程中調用HAL_UART_TxCpltCallback()函數進行邏輯處理是以用阻塞模式更友善一些吧,會不會産生其他問題待後續研究。

1.4.2 出現的一些疑惑

NUCLEOL4R5ZI通過USB連接配接至電腦序列槽時,在實作系統供電的同時,可以虛拟出一個序列槽實作與PC之間的資料通信,可以了解為自帶一個類似于CH340的序列槽驅動。這是個非常友善的功能,LPUART1用的引腳為PG7、PG8,如果我将這兩個引腳接到如L610這樣的4G子產品中,或者NB-IoT這樣的子產品中可以實作在給子產品發送指令的同時,通過電腦序列槽進行監控,而不需要再使能額外的序列槽和接額外的線。但是出問題也出在這裡。

①将USB轉TTL子產品通過PG7、PG8連接配接到NUCLEO闆子上同時通過USB線連接配接闆子,相當于接了兩個序列槽驅動,但是用的同一個序列槽——LPUART1,(打開XCOM序列槽調試助手通過虛拟序列槽通信和SSCOM序列槽調試助手通過CH340進行通信,雖然通過不同的序列槽驅動通信,實際上都是和LPUART1進行通信,不知道讀者能不能明白我的意思)測試發現,序列槽調試助手通過虛拟序列槽可以實作資料的收和發(表現為:通過序列槽調試助手向單片機發送資料,會在目前界面傳回發送的内容,同時通過CH340驅動通信的界面也會傳回剛剛發送的内容),但是通過CH340序列槽進行通信的時候,隻能收,不能發(表現為發送資料,序列槽沒有任何反應)。

一開始我以為是闆子的PG8引腳壞了,還特意換了一個沒拆封過的闆子,重新測試了結果依然相同,後來我又用同樣的方式測試了USART3,可以通過CH340驅動通信進行資料的收和發,證明USB轉TTL子產品沒有問題,且序列槽配置沒有問題,但是LPUART1就是會出現這個問題。

②Receive_IT的問題:将注冊的RxBuffer裡面的位元組長度設定為6時,第一次隻接收了4個位元組就觸發了接收中斷,之後是每接收6個位元組觸發一次接收中斷;位元組長度設定為4時,每次都隻接收2個位元組就觸發一次接收中斷,設定為1時現象倒是正常的,發送什麼傳回什麼,以上說的這些,用USART3序列槽進行同樣的測試,一切正常。

綜上所述,LPUART1是很奇怪的序列槽,不明白是什麼原因,待進一步研究,希望有懂的大佬不吝賜教,為了序列槽通訊友善,我曾将自動生成的代碼裡的波特率改為115200,因為序列槽調試助手裡的波特率沒有原代碼裡設定的209700選項,不知道是不是這個原因,可是還是不能解釋LPUART1不正常而USART3正常的問題。歡迎交流學習!

1.4.3 解決問題的思路

1.4.3.1 問題描述

使用序列槽通信最終的目的是與通信子產品進行AT指令通信,通過通信子產品傳回給序列槽的資料進行邏輯上其他處理,如連接配接伺服器,發送和訂閱相關主題等等,這就意味着我需要對RxBuffer的内容進行處理,而不是簡單的傳回序列槽我發送的内容即可,意味着我在注冊中斷接收的時候設定的位元組長度必須能包含我接收到全部資料,是以,使用了255長度,這就導緻了一個問題,序列槽隻有在接收到255位元組長度資料後才會調用callback函數,無法及時通過序列槽監控程式程序。

再考慮到之前的PG8引腳無法接收資料,是以改用USART3序列槽的PD8、PD9引腳分别實作發送和接收,在通過USART3發送資料的時候,同時通過LPUART1發送一遍相同的資料,實作通過LPUART1序列槽同步監控USART3序列槽的通信程序。

接下來流程如:單片機通過USART3序列槽向L610子產品發送AT指令:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

L610接收到後傳回給序列槽一個包含“OK”的字元串,這個字元串将被存至定義好的長度為255的RxBuffer中,之後通過定義好的strx指針接收函數strstr判斷的結果,如果RxBuffer中含有指定的“OK”字元串,則strx不為空,後面可以根據strx指針是否為NULL進行進一步地程式設計。

strx=strstr((const char*)RxBuffer,(const char*)"OK");
           

在進行過一次AT指令的互動後,需要對RxBuffer進行清空操作,否則可能會溢出,或者影響下一次的傳回值判斷:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

這裡的問題就出來了,因為HAL庫良好的封裝性,暫時沒法從寄存器層面獲得每一次位元組傳輸的過程,即無法得知寄存器内資料長度,導緻清空資料庫每次都需要清255,從程式設計上将是很不合理的。如果單獨寫一個判斷RxBuffer資料長度的函數,從RxBuffer[0]開始判斷到0x00為資料長度的話,如果接收到的資料中包含空行(實際測試中确實有這種情況,傳回的“AT OK”中間隔了0x00的空行)就會導緻清空緩存失敗,暫時沒想到好的辦法,是以隻能每次都清255。

本來此種設計除了每次都需要清255的RxBuffer不合理之外,從其他方面考慮應該是沒什麼問題的,但是在實際測試的時候,發現有些AT指令很快就可以識别,有些卻非常慢,甚至需要發送很多遍才能識别到RxBuffer中接收到的傳回值,導緻通信子產品的聯網過程非常慢,如下圖所示:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

伺服器接收到了四次資料:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

這裡又是一組沖突,如果是RxBuffer接收資料程式設計的問題,為什麼有的AT指令一遍就過了?如果不是這個問題,那麼為什麼有的AT指令需要發送十幾二十幾遍才能識别出來?更有甚者,聯網已經連上了,在向伺服器發送資料的時候隻要發送一遍,接收到“+MQTTPUB”傳回值即止,然而單片機發送了好幾遍,伺服器也确實收到了好幾遍之後程式才從RxBuffer中接收到了傳回值,這就離譜,希望有懂的大佬指教一下。

1.4.3.2 解決方案

問題既然已經出現了,這麼慢的聯網速度也是我無法接受的,于是我在尋求其他解決方法的時候,看到了這樣一篇博文:https://blog.csdn.net/weixin_44578655/article/details/104677301,其中有這樣一段:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

這讓我深受啟發,這不是标準庫中的寫法麼,于是按照如下步驟進行了HAL庫中代碼的修改:

①注釋掉原來在序列槽初始化末尾處注冊的接收中斷,在同樣的地方替換為引用博文中所述的使能接收中斷語句:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

②在it.c檔案中的USART3中斷處理函數中注釋掉CubeMX自動生成的通用中斷函數入口(我是直接照着此前使能的LPUART1序列槽寫的,從CubeMX中打開序列槽全局中斷使能應該也是一樣的效果),并替換為标準庫寫法:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

查詢STM32L4的序列槽寄存器,引用博文中的SR寄存器在L4中對應的為ISR寄存器,DR寄存器對應的RDR寄存器,對代碼進行了修改。此外,在這裡借助寄存器對RxBuffer進行指派,不僅可以逐位元組進行指派,還可以借助RxCounter得知RxBuffer的長度,在清除RxBuffer緩存的時候,可以将255替換為RxCounter提高程式運作效率。

之後燒錄程式進行測試,發現程式效率奇高,效果非常好(為了作對比,雖然得知了RxBuffer的數組長度為RxCounter,但是RxBuffer清空函數我并未修改,仍為清255,但是兩種方式的實際運作效果差别還是非常大的),如下圖所示:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

可以看到伺服器接收到了一次資料:

【HAL庫】序列槽通信HAL庫入門之序列槽通信

寫在最後

本來隻想簡單的記錄一下的,不知不覺就寫了這麼多,最主要的目的是培養自己整理學習經驗的習慣,防止後人踩坑。HAL庫系列的内容,後面如果繼續學習會繼續以【HAL】的字首撰寫學習筆記

關于HAL庫,目前個人的看法是有好有壞,最大的好處是實作了“解耦”,利用代碼生成器,圖形化方式一鍵配置可以省去很多事情,在平台間的移植能力也會變強,實作“解耦”是一件不容易的事情,新事物的出現必然有其出現的理由,不能害怕也不應該抵觸,勇于嘗試、了解,才能揚長避短,提高自己的開發能力。至于壞處,有些人說什麼編譯慢、什麼簡單的問題複雜化之類的,其實最大的問題是資料和教程太少了,希望大佬們在學習的過程中可以分享一下學習經驗吧,精衛填海,從我做起!

繼續閱讀