您現在的位置是:首頁 > 動作武俠首頁動作武俠

IOT安全(二)——再探stm32

簡介RCC_CR、RCC_CFGR是其中的關鍵,它們是所有部件的“頭頭”,它們來控制諸如HSE、PLL等裝置的啟動與否,分頻、倍頻的大小,此外,APB、AHB等等都有相應的暫存器,我們需要按照前一篇文章的辦法,把他們統統寫出來

什麼是中斷優先順序

IOT安全(二)——再探stm32

上一篇文章中我們實現了stm32的gpio操作,這次我們將更進一步,繼續完成對題目的復現。

透過之前的學習相信大家都掌握了學習閱讀官方手冊的能力,在本篇中為了節約篇幅不再進行對暫存器的位、暫存器程式碼編寫的詳細說明,而是著重講解原理和背後的知識。

簡單的時鐘樹

對於任何的計算機來說,時鐘都是其至關重要的一環,在任何stm32的程式啟動前,都必須要進行時鐘的初始化操作,好在stm32的時鐘較為簡單,這次我們的目標就是編寫我們自己的時鐘初始化函式,來替換掉stm32cube為我們生成的程式碼。我們先來看一下手冊中給出的時鐘樹圖:

IOT安全(二)——再探stm32

可以看到時鐘樹整體並不複雜,但是因為stm32提供了包括usb、dma在內的多種功能,所以會用到多種頻率不同的時鐘訊號,因此圖中有很多涉及到時鐘頻率縮放的部件,這是我們要關注的一個點,此外,可以看到外設部分幾乎都是有sysclk進行簡單變化後得到的,所以sysclk也是我們關注的重點。

從最左邊開始,首先是osc_in、osc_out、osc32_in、osc32_out、mco五部分引腳,其中帶osc的我們在上一篇文章中利用stm32cube自動生成過,osc代表是來自外部的晶振,晶振透過高頻震盪來提供有規律的訊號,也就形成了時鐘訊號。實際上,在晶片內部也有晶振來提供時鐘訊號,但由於複雜原因,晶片內部往往難以整合高頻率、高精度的晶振,無法發揮晶片的全部效能,這就需要我們在外部新增高頻高精度的晶振來提供時鐘訊號。

mco是時鐘輸出引腳,可以將時鐘訊號輸出,透過配置相應的暫存器即可實現對不同時鐘訊號的輸出。我們暫時用不到這個功能。

從osc接入就是真正的時鐘源了,外部晶振帶來的時鐘有兩種:

HSE是高速外部時鐘的簡寫,晶振頻率可取範圍為4~16MHz

LSE是低速外部時鐘的簡寫,一般採用32。768KHz

上面我們也說了,除了外部接入的,還有內部整合的,也是兩種:

HSI是高速內部時鐘的簡寫,由內部RC振盪器產生,頻率為8MHz,非常非常的不穩定。

LSI是低速內部時鐘的簡寫,同樣由內部RC振盪器產生,頻率大約為40KHz

IOT安全(二)——再探stm32

首先來看下半部分,對於LSI來說,它分了兩條線

IWDG(獨立看門狗)的時鐘源。

用來作為RTC(即實時時鐘)的可選項,在時鐘樹中,梯形表示可選項,上圖中可以看到RTC有三個可選項。

這裡簡單說一下實時時鐘是個啥,它“實時”的意思是它掉電後還繼續執行,不受限制,除此之外,他就是個普通的計時器。功能很簡單,但它的電路很巧妙,有興趣的同學可以查檢視看,它經常被用來實現時間戳、記錄當地時間等功能。

對於LSE來說,它只是作為RTC的一個可選項,除此之外,HSE也作為RTC其中一個選項,不過其頻率需要除以128,將原來高頻的訊號,透過電氣元件,成倍的降低它們的頻率,最終將原來的頻率降低到我們想要的大小,也就是”分頻“的意思,這是整個時鐘樹中最常出現的部件,圖中的prescaler是“預分頻”的意思,就是在某個需要的訊號前對其輸入訊號進行“分頻”。

IOT安全(二)——再探stm32

再來看上半部分,首先就是pll這個概念,它的全稱是鎖相環倍頻輸出,所謂”鎖相“即保持處理後的訊號與原來的基準訊號的相位一致,而”倍頻“就是說要提高頻率。從圖中可以看到,HSE經過處理後到PLLXTPRE部件,然後和HSI共同組成pllsrc的候選,最終經過PLLMUL放大頻率(最高可達16倍),輸出pll時鐘,在別的資料中往往把pll時鐘和上面提到的四種時鐘共同作為stm32的時鐘源,但是我們可以看到,實際上pll時鐘最終還是來自HSE和HSI,pll在整個時鐘樹中最大的作用就是拉高了訊號的頻率,HSE雖然名字中帶有高速,但對於某些裝置(比如USB)來說還是遠遠不夠用的,所以需要pll來倍頻。

再往後就是sysclk,即系統時鐘,聽這名字就知道它的地位,基本上所有的外設的時鐘訊號,都是透過sysclk的分頻得到的。它同樣有三個候選人,分別是HSI、pll時鐘、HSE,從圖中可以看到它最高可以達到72MHZ。

而下面的css是系統時鐘的監視器,因為系統時鐘和外設息息相關,一旦發生問題將導致整個系統的奔潰,所以設定了css,當HSE失效時(HSE畢竟是來自外部的晶振,失效的可能性要遠大於HSI),它會自動讓系統時鐘的來源切換為HSI,從而保證系統的穩定。

IOT安全(二)——再探stm32

再向左看就到了外設部分的,usb很簡單,只是經過了分頻就得到了最終的訊號,其餘的主要有兩條“線”

AHB,全稱是Advanced High performance Bus,是一種匯流排,有別的部件掛載在上面,它頻率高速度快,因此是“高效能”的匯流排,有點類似計算機的北橋,主要連線高速裝置,比如記憶體、dma等。它是支援多主裝置的匯流排,可以有多個主模組,資訊由主模組流向從模組。

ABP,全稱是Advanced Peripheral Bus,它類似計算機的南橋,效能、頻率較低,連線的都是SPI,I2C等裝置,可以看到,APB分為了ABP1、ABP2兩個裝置,這是因為它不像AHB那樣支援多主模組,它的主模組就是ABP,ABP再有兩個從模組ABP1、ABP2,這兩個從模組分別負責不同的裝置掛載。

最後來看看mco,可以看到它來源自HSE、HSI、PLL、SYSCLK,我們可以透過設定暫存器,來讓它輸出這四種時鐘訊號。

到此為止我們就梳理完了整個stm32的時鐘樹,千萬不要忘了上一篇文章中我們說過的重要的一點:stm32的一切都離不開暫存器,時鐘也是如此,查閱手冊我們可以看到時鐘相關的暫存器。

IOT安全(二)——再探stm32

RCC_CR、RCC_CFGR是其中的關鍵,它們是所有部件的“頭頭”,它們來控制諸如HSE、PLL等裝置的啟動與否,分頻、倍頻的大小,此外,APB、AHB等等都有相應的暫存器,我們需要按照前一篇文章的辦法,把他們統統寫出來。這裡因為程式碼是在是太多了,所以我只放出我寫的一小部分

#ifndef __SysInt#define __SysInt#define PERIPHY_BASE ((uint32_t)0x40000000)#define ABP1PERIPHY PERIPHY_BASE#define ABP2PERIPHY (PERIPHY_BASE + 0x10000)#define AHBPERIPHY_BASE (PERIPHY_BASE + 0x20000)#define RCC_BASE (AHBPERIPHY_BASE + 0x1000)#define __IO volatiletypedef unsigned int uint32_t;void SetClockConfig(void);typedef struct { uint32_t HSION :1; uint32_t HSIRDY :1; uint32_t Reserved0 :1; uint32_t HSITRIM :5; uint32_t HSICAL :8; uint32_t HSEON :1; uint32_t HSERDY :1; uint32_t HSEBYP :1; uint32_t CSSON :1; uint32_t Reserved1 :4; uint32_t PLLON :1; uint32_t PLLRDY :1; uint32_t Reserved2 :6;}CR_Bit;typedef struct { uint32_t SW :2; uint32_t SWS :2; uint32_t HPRE :4; uint32_t PPRE1 :3; uint32_t PPRE2 :3; uint32_t ADCPRE :2; uint32_t PLLSRC :1; uint32_t PLLXTPRE :1; uint32_t PLLMUL :4; uint32_t USBPRE :1; uint32_t Reverse0 :1; uint32_t MCO :3; uint32_t Reverse1 :5;}CFGR_Bit;typedef struct { uint32_t LSIRDYF : 1; uint32_t LSERDYF : 1; uint32_t HSIRDYF : 1; uint32_t HSERDYF : 1; uint32_t PLLRDYF : 1; uint32_t Reverse0 : 2; uint32_t CSSF : 1; uint32_t LSIRDYIE : 1; uint32_t LSERDYIE : 1; uint32_t HSIRDYIE : 1; uint32_t HSERDYIE : 1; uint32_t PLLRDYIE : 1; uint32_t Reverse1 : 3; uint32_t LSIRDYC : 1; uint32_t LSERDYC : 1; uint32_t HSIRDYC : 1; uint32_t HSERDYC : 1; uint32_t PLLRDYC : 1; uint32_t Reverse2 : 2; uint32_t CSSC : 1; uint32_t Reverse3 : 8;}CIR_Bit;typedef struct { uint32_t LSION : 1; uint32_t LSIRDY : 1; uint32_t Reverse0 : 14; uint32_t Reverse1 : 8; uint32_t RMVF : 1; uint32_t Reverse2 : 1; uint32_t PINRSTF : 1; uint32_t PORRSTF : 1; uint32_t SFTRSTF : 1; uint32_t IWDGRSTF : 1; uint32_t WWDGRSTF : 1; uint32_t LPWRRSTF : 1;}CSR_Bit;typedef struct{ __IO CR_Bit CR; __IO CFGR_Bit CFGR; __IO CIR_Bit CIR; __IO APB2RSTR_Bit APB2RSTR; __IO APB1RSTR_Bit APB1RSTR; __IO AHBENR_Bit AHBENR; __IO APB2ENR_Bit APB2ENR; __IO APB1ENR_Bit APB1ENR; __IO BDCR_Bit BDCR; __IO CSR_Bit CSR;}RCC_Type;

大概300多行結構體定義完畢後,我們就可以開始寫時鐘的初始化程式碼了,實際上,只要明白了邏輯,這部分程式碼並不難寫。

首先我們需要明確的第一件事:誰才是最先啟動的時鐘?顯然外部晶振是不能擔此重任的,你總不能外部晶振不啟動你就不幹活了對吧?所以啟動的必然是HSI和LSI,只有他們幹活了我們才能繼續工作,透過RCC暫存器,我們首先進行HSI使能,然後要寫個while迴圈,一直等待直到HSI準備就緒才能開始下一步,程式碼如下

//使能HSI RCC->CR。HSION = 1; //等待HSI就緒 while(!RCC->CR。HSIRDY);

接著就是啟動外部晶振了,和上面的操作類似,我們這裡同樣while迴圈寫死。

//使能HSE RCC->CR。HSEON = 1; //等待HSE就緒 while(!RCC->CR。HSERDY);

接下來就是對各個裝置進行簡單的分頻、倍頻設定,我們根據自己的實際情況進行調整即可,我寫了詳細的註釋,程式碼如下:

//調整低速APB預分頻(APB1)為2分頻 //調整高速APB預分頻(APB2)為不分頻 //調整AHB預分頻為不分頻 //調整ADC預分頻為2分頻 RCC->CFGR。PPRE1 = 4; RCC->CFGR。PPRE2 = 0; RCC->CFGR。HPRE = 0; RCC->CFGR。ADCPRE = 0; //調整PLL輸入時鐘源為HSE RCC->CFGR。PLLSRC = 1; //調整PLL倍頻係數為9 RCC->CFGR。PLLMUL = 7; //使能PLL時鐘 RCC->CR。PLLON = 1; //等待PLL時鐘就緒 while(!RCC->CR。PLLRDY); //調整SYSCLK為PLL RCC->CFGR。SW = 2; //等待SYSCLK為PLL while(RCC->CFGR。SWS!=2);

到此,我們就簡單了實現了我們自己的時鐘初始化,對應如下stm32cube自動生成的函式,替換即可。

RCC_DeInit(); //初始化RCCRCC_HSEConfig(RCC_HSE_ON); //設定HSEHSEStartUpStatus = RCC_WaitForHSEStartUp(); //等待HSE就緒RCC_HCLKConfig(); //設定AHB時鐘RCC_PCLK2Config(); //設定高速AHB時鐘RCC_PCLK1Config(); //設定低速AHB時鐘RCC_PLLConfig(); //設定PLLRCC_PLLCmd(ENABLE); //啟用pllwhile(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) //等待PLL工作RCC_SYSCLKConfig(); //設定系統時鐘while(RCC_GetSYSCLKSource() != 0x08) //判斷是否系統時鐘源是否為PLLRCC_APB2PeriphClockCmd()/RCC_APB1PeriphClockCmd() //啟動外設時鐘

有些複雜的中斷

在題目中使用usart傳輸flag,但usart作為一種通訊手段,它最終還是依賴於中斷機制,所以我們必須得先研究明白stm32的中斷機制,才能更好的學習usart的使用。對於stm32來說,由於它依賴於ARM的核心,所以它的中斷機制與ARM核心息息相關,以M3核心為例:

m3支援256箇中斷,而stm32在基礎上進行了刪減,使用了84箇中斷;

m3支援256級的中斷優先順序,可以做到每一箇中斷都有一個優先順序,而stm32只保留了16級

IOT安全(二)——再探stm32

我們在上一篇文章中,利用stm32cube自動將其中的一個GPIO引腳“進化”為了中斷exti1,大概分成了三步

初始化exti

初始化NVIC

編寫中斷服務函式

第一步中的exti是外部中斷的意思,在stm32的84箇中斷中,有64個屬於外部中斷,嚴格來說,stm32每一個GPIO引腳都可以“進化”為外部中斷,但是它規定中斷以組為單位,比如PA1、PB1、PC1、PD1、PE1、PF1、PG1為一個組,圖中的exti1就表示這是第一組中斷組,每一個組同一時間只能有一個代表出場,一旦選擇了PA1,那麼剩下的組員就不能再“進化”了。

所謂“進化”,實際上就是將相應的埠對映到相應的外部事件,端口出現變化就會觸發對應的外部事件,進而到外部事件的中斷服務程式。可以參考下圖中exit的連線方式,每一組的引腳連線到一個選擇器上,然後交由對應的EXTI處理

IOT安全(二)——再探stm32

第二步是初始化NVIC,NVIC是巢狀向量中斷控制器的意思,他是所有中斷的歸屬,不管是上面的exti還是usart、usb等等最終都是NVIC,然後由NVIC傳遞給cpu處理,最後透過flash中斷向量表確定對應中斷對應的函式地址,跳轉到對應的中斷處理函式。NVIC提供了 43箇中斷通道(通道以提前分配好,比如exti1就是第7個通道,查閱手冊即可),箇中斷通道都具備自己的中斷優先順序控制位元組PRI_n,每4個通道的8位中斷優先順序控制字構成一個32位的優先順序暫存器,透過設定暫存器即可改變對應的中斷優先順序,當有兩個以上的中斷到來時,NVIC會根據中斷的優先順序進行抉擇。

但實際上,stm32雖然每個通道提供了8位中斷優先順序,但由於它本身只支援16種優先順序,所以只有高4位是有效的,而高4位也頗有講究,分為了搶佔式優先順序和響應優先順序,高位為搶佔式優先順序,低位為響應式優先順序(可以任意位數,比如1和3、2和2、4和0),他們的關係有點複雜:

具有高搶佔式優先順序的中斷可以在具有低搶佔式優先順序的中斷處理過程中被響應,也就是實現了中斷巢狀。

同樣的搶佔優先順序的中斷如果同時到來,就根據響應優先順序判斷,實現了中斷的判優

自動生成的程式碼如下:

NVIC_InitTypeDefNVIC_InitStructure;NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //選擇中斷分組2NVIC_InitStructure。NVIC_IRQChannel= EXTI1_IRQChannel; //選擇exti1NVIC_InitStructure。NVIC_IRQChannelPreemptionPriority= 0; //搶佔式中斷優先順序設定為0NVIC_InitStructure。NVIC_IRQChannelSubPriority= 0; //響應式中斷優先順序設定為0NVIC_InitStructure。NVIC_IRQChannelCmd=ENABLE; //使能中斷

第三步是編寫中斷服務函式,這一步就沒啥好說的了,寫就完事了,唯一要注意的是,由於中斷通道中EXTI0 – EXTI4這5個外部中斷在不同的通道中,所以有著自己的單獨的中斷響應函式,EXTI5-9共用一個通道,也就只有一箇中斷響應函式,EXTI10-15也是共用一箇中斷響應函式。

1 void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource) 2 { 3 uint32_t tmp = 0x00; 4 /* Check the parameters */ 5 assert_param(IS_GPIO_EXTI_PORT_SOURCE(GPIO_PortSource)); //assert_param是對引數進行有效性檢查 6 assert_param(IS_GPIO_PIN_SOURCE(GPIO_PinSource)); 7 8 tmp = ((uint32_t)0x0F) << (0x04 * (GPIO_PinSource & (uint8_t)0x03)); 9 AFIO->EXTICR[GPIO_PinSource >> 0x02] &= ~tmp;10 AFIO->EXTICR[GPIO_PinSource >> 0x02] |= (((uint32_t)GPIO_PortSource) << (0x04 * (GPIO_PinSource & (uint8_t)0x03)));11 }

上述自動生成的程式碼是將PD11,PD12對映到外部事件上,9、10行是關鍵,透過與或操作,將暫存器的值改為對應的值,進而實現目的,如果你已經按上一篇文章中所說的將GPIO及中斷的暫存器全部寫作結構體的話,只需要檢視手冊將相應的結構體賦值即可。

上述中斷都是exti的內容,除了exti,還有usart等一堆的中斷,這些中斷不需要和外部事件對映即可使用,也就是改寫了第一步,首先開啟對應中斷的,再交由NVIC,最終跳轉至對應的處理函式。

IOT安全(二)——再探stm32

usart原理或許有些複雜,但單純去寫一個帶有usart的程式碼卻非常容易,這裡我們就不再使用暫存器做操作了,我們直接採用中斷的庫函式來進行操作。我們在stm32cube中開啟usart功能,生成的程式碼中會新增usart庫,裡面提供了資料傳送、資料接收等功能,這些不是我們本節的重點,主要先來看看中斷部分

HAL_UART_TxHalfCpltCallback(); //一半資料傳送完成後的回撥函式。HAL_UART_TxCpltCallback(); //全部資料傳送完成後的回撥函式HAL_UART_RxHalfCpltCallback(); //一半資料接收完成後的回撥函式。HAL_UART_RxCpltCallback(); //全部資料接收完成後的回撥函式HAL_UART_ErrorCallback(); //傳輸過程中出現錯誤時的回撥函式。

我們只需要編寫這些回撥函式即可實現對應的功能,其他的諸如usb、dma等等也是類似,用起來相當方便。

小結

到此我們已經實現了題目中大部分內容,最後就是usart的資料傳輸了,下一篇文章中我們將重點討論usart的原理與使用,並以usart為出發點,再來看看其他stm32有趣的地方

Top