之前做STM32的項目, 一直都用的是標准固件庫。最近有個比較簡單的項目,就想試試ST強推的開發工具cubemx。
用了下來,感覺CUBEMX的 HAL庫做得很模塊化,讓一些用戶遠離了底層。但是也有缺點:
1. 各種模塊,應用都層次化了,所以調用關系也比自己寫繁瑣得多。
2.雖然簡化了很多應用的開發過程,但也是因為把驅動模塊化了,但不靈活,面對一些特殊點的場合,就容易出現問題。
3.一旦代碼出問題,找起故障來很麻煩,在各種函數中跳來跳去。比如我在SPI中遇到問題,要查故障,從總中斷,跳到TX子服務,然后又執行一個注冊的中斷處理函數,最后去執行用戶回調函數。跳來跳去的。
結論: 即便是用CUBEMX來做項目,還是需要看STM32的用戶手冊,去了解各種寄存器,各種外設的特點,不然只知道簡單用法,不知道執行原理,是沒辦法排查故障和實現任務的。
好了,現在說說STM32用來做SPI從站的問題。
一 CUbe MX生成代碼
首先把SYS,時鍾等設好(不多說了)。然后就開始SPI的設置。
我把SPI pin 設置為從站,關閉 NSS,並將PA4作為EXTI4 。使用EXTI4作為一個數據幀的起始標志。
當然,也可以不用EXTI4做幀起始,而用定時器來識別幀的起始字節( 通信時間間隔大於XXMS,代表開始了新的通信幀)。
在 configuration中,做參數設置:
NVIC Setting中開啟SPI中斷。
最后在project菜單里,執行Generate Code ,生成代碼即可。
二 如何在程序中實現基本的SPI通信。
對於基礎運用,相當簡單:
在主函數中,執行:
HAL_SPI_TransmitReceive_IT(&hspi1, TXbuf,RXbuf,CommSize);
當SPI上出現了 CommSize個字節的數據后,中斷函數會調用回調:
HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
這在HAL中是個弱申明的函數,我們只需要做一個自己的同名回調函數,實現預定功能即可,編譯時會自動替換。
就靠這兩個函數,基本的通信就沒問題了。
三 是不是很簡單?!然而事實是殘酷的,這還沒完。SPI從站的問題:
如果就這樣使用,你會發現從站數據(MISO),有時能准確到達主站,有時卻整體后移了一個字節導致數據不完整。並且回調函數也不是很靈,時而執行,時而不執行。
跟蹤代碼,你會發現,執行中,有兩種情況:
1. 執行HAL_SPI_TransmitReceive_IT 后,即便主站沒有發起通信,從站也立刻跳入了TX_ISR中斷。 這種情況,程序執行時正常的。
2.執行HAL_SPI_TransmitReceive_IT 后,未跳入TX_ISR。當主站發起通信時,從站先跳入TX中斷執行DR=S0,然后跳入RX中斷執行R0=DR。看起來沒問題,然而,結果就是主站收到的數據不正確。而且HAL_SPI_TxRxCpltCallback回調函數頁沒有執行,HAL認為 “故障:DR寄存器中還有數據沒有發出”。
情況2是如何產生的呢?簡單的說,就是某次通信出現故障后,TXE無法正確清空(置1),導致通信錯位。
具體的說,就要從STM32的SPI的通信原理說起了。
一次正確的執行流程,時序圖是如下的:
是不是看起來有點懵?
簡單的說,就是需要按照如下順序做通信:
1.通信前,執行HAL_TransmationRecive_IT() .
2. 由於DR此時一般為空,所以產生了TXE中斷 ,將SendByte0 先寫入DR寄存器。
3.當第一個CLK到來時,DR中的SendByte0會放到總線上,此時,DR空就會產生一個TXE中斷!將SendByte1寫入DR寄存器。
4.再執行7個CLK 。
5.第8個CLK完成后,發生RX中斷,DR= RecByte0!
6.第9個CLK,又發生了TXE中斷...
上面是正常情況,此時 ,TXE中斷需要比 RXE中斷先產生兩個回合,才能保證SendByte 和 RecByte都依次完整到傳輸!
如果由於某種原因(比如上次通信失敗),TXE最初是0,則發送DR會丟失第一個中斷。這將導致SendByte推遲一個字節!
可惜的是,TXE是只讀寄存器,無法通過手動清理來使其恢復為空。也就是說,一旦通信發生過一次錯誤,那么,在使用HAL庫的情況下, 這個錯誤就無法消除。
並且,由於丟失了第一個TXE中斷,HAL在計算已發送字節的時候,會發現少發送了一個字節,這會導致回調函數“HAL_SPI_TxRxCpltCallback”無法執行。
四 解決的辦法
一種解決辦法:對HAL庫做修改
在HAL_TransmationRecive_IT()中, 增加代碼,判斷TXE是否空。如果空,則正常執行; 如果為滿,則直接把SendByte0寫入DR寄存器,並執行TxXferCount--;相當於手動執行了第一次中斷服務。
但這個辦法有個麻煩的地方,就是以后每次使用CUBEMX填寫功能塊時,編譯器會自動把HAL_TransmationRecive_IT()函數恢復,我們就需要不斷的去修改它的代碼。
另一種解決辦法:
不論TXE的狀態,先執行 DR = SendByte0;
然后,再執行HAL_TransmationRecive_IT()。此后,在SPI通信前,TXE將會一直保持滿的狀態。
Hspi-> TxXferCount-- ,手動減少發送倒計數。
這個方法的好處是,不用修改HAL代碼,更新程序不受影響。
可見,如果對SPI的執行原理不了解, 單純使用HAL,還是容易出現一些問題的。
使用HAL,雖然讓項目更快,普通情況下也更安全,但是為了解決一些特殊的情況,我們還是要掌握STM32的用戶手冊,多看看寄存器和執行原理。
--------------------------雖然做了上述修改,但是程序還是有很小的幾率出現數據字節錯位的情況,后來又仔細研究了下,發現DMA的處理沖突,和中斷的優先級,影響也很大-----------
1.DMA優先級(如果使用DMA的話)
CUBEMX默認的SPI DMA優先級很低,當驅動中有別的MDA設備時,會阻礙SPI DMA執行,導致收發錯字節。
所以在CUBEMX中一定手動設置:
SPI_DMA_RX : veryhight
SPI_DMA_TX : veryhight
2.中斷優先級
必須把SPI的DMA中斷優先級調整到最高:“0” ,否則SPI的中斷很可能被阻塞導致字節了漏發漏收,以及收發錯字節。(如果使用DMA的話)
如果沒有使用DMA的話,就應該把SPI的處理程序中斷優先級調到最高。
3.如果使用了EXIT作為片選信號的觸發,那么應該在EXIT的中斷處理程序中,去接收SPI。
以往,我們習慣的做法是,在EXIT中斷中,做一個標志位,然后在主循環區處理SPI接收。但這個做法對SPI從站來說,容易產生處理程序延遲,錯過數據。所以應該在中斷處理程序中接收。