1. 程式人生 > 其它 >STM32(2):點亮LED(下)

STM32(2):點亮LED(下)

本文摘自:
https://blog.csdn.net/xiashiwendao/article/details/122292404

概述

點亮LED表面看起來貌似很簡單,但是如何想要搞清楚其背後牽涉的每一行程式碼的具體含義,還是需要花費一些功夫的,而且,只有把LED的背後只是搞清楚了,才算嵌入式開發的基礎入門。
今天我們就來研究一下LED的重頭戲,RCC_Init;什麼是RCC?上手冊:

RCC

RCC,Reset and Clock Control,重置以及時鐘控制;STM32手冊使用了兩個章節來對其進行描述,可見它的重要性;對於RCC的初始化也是比較複雜,裡面包含了STM32對於時鐘的相關機制,

程式碼總覽

void RCC_init(uint16_t PLL)
{
	uint32_t temp=0;  

	*((uint32_t *)RCC_CR) |= 0x00010000; 
	while(!( *((uint32_t *)RCC_CR) >>17));

	*((uint32_t *)RCC_CFGR) = 0X00000400;

	PLL -= 2;
	*((uint32_t *)RCC_CFGR) |= PLL<<18;   

	*((uint32_t *)RCC_CFGR) |= 1<<16;

	*((uint32_t *)FLASH_ACR)|=0x2;
	*((uint32_t *)RCC_CR) |= 0x01000000;
	while(!(*((uint32_t *)RCC_CR) >> 25));

	*((uint32_t *)RCC_CFGR) |= 0x00000002;
	while(temp != 0x02)
	{  
		temp = *((uint32_t *)RCC_CFGR) >> 2;
		temp &= 0x03;
	}   
}

使能HSE

第一行有效程式碼,是熟悉的味道,前一節我們說過,或運算一般用於設定指定位(而且還不影響其他位)

*((uint32_t *)RCC_CR) |= 0x00010000; 

看到了RCC_CR,首先就是翻手冊,RCC的章節的RCC_CR章節:

然後檢視暫存器內部32位的定義:

0x00010000轉換為32bit的二進位制:0000 0000 0000 0001 0000 0000 0000 0000;這裡有一個小技巧,就是學會分割來看,手冊裡面的暫存器的定義一般定義是分為上下兩行的;從16進位制來看,從左往右,前面4為是負責上面RCC_CFGR的高位16bit,後面4位是對應(二進位制)低位16bit,即下面的16bit;對此次而言,低4位都是0,可以不需要care,直接關注高4位即可,其中高4為只有最後一位是有效設定,1對應的四位二進位制是0001,即16位設定為1,檢視暫存器定義的16位對應的是HSEON位,即使能HSEON。

HSE和時鐘

什麼是HSEON?需要拆開來看:HSE ON,HSE是High Speed External,外部高速時鐘,所以HSEON就是使能外部高速時鐘;
為什麼要配置HSE呢?因為在硬體系統中,各個部件的工作、協調,都是基於系統時鐘的,比如我們通常講的CPU的速度快慢,就是指CPU的時鐘頻率,即每秒鐘能夠工作多少個時鐘;

小貼士:

在win10系統中可以看到有兩個頻率,如下圖,分別是1.9GHz以及2.11GHz;其中第一個是Intel提供的標準頻率,其實是CPU名字的一部分,第二個頻率是win10系統自己計算出來的,很多時候和Intel提供的頻率值並不相等;不過兩者相差也不會太大。

系統時鐘有三個來源,分別是HSE(High Spped External),HSI(High Speed Internal)以及PLLCLK,引入了PLL就是因為很多時候,需要基於系統時鐘進行分頻倍頻,比如有的裝置(RAM,DMA)需要比系統時鐘快,於是需要通過PLL來進行加倍頻率,還有的低俗的裝置需要低於系統頻率工作,也是通過PLL來進行減速,這樣只需要一個系統時鐘就可以同時滿足高速和低俗裝置。

所以PLL的時鐘源還是HSE,HSI,不過經過PLL倍頻調節輸出的時鐘稱之為PLLCLK;
再回到我們的程式碼裡面,這裡配置時鐘源就是HSEON(如果需要使用PLL則還需要將PLLx位配置為1);

確認HSE使能生效

完成了時鐘源的配置,下面一步是等待HSEON的配置的生效:

while(!( *((uint32_t *)RCC_CR) >>17));

即第等待第18位(編號17)HSERDY的值變成1,當且僅當配置HSEON生效之後,該位置才會由硬體設定為1,注意在暫存器定義裡面HSERDY配置為“r”,這個代表軟體層面是無法改變這個bit的值,只能夠讀取:

然後再檢視暫存器定義裡面對於這一位的解釋,0就是HSEON位設定並未生效,1就是設定已經生效:

等待的過程是使用位運算裡面右移“>>"實現的,右移的運算規則裡面是低位直接丟棄,例如11111右移4bit,最後結果就是1;這行程式碼的邏輯意義就是RCC_CR右邊17位直接丟棄,即016為拋棄,只是保留了3118位(注意數字方向,是從左向右逐次減小的,和暫存器定義一致),共計15bit;
又因為上一步驟中通過和0x00010000進行與運算將除了HSEON位之外的位置全部置為“0”了,所以之類RCC_CR右移17位之後,包括HSERDY在內的位全部都是0,只有當HSEON生效之後,HSERDY才是1,即*((uint32_t *)RCC_CR) >>17 = 1,於是此時退出while迴圈。

配置其他時鐘

CR位配置解決了,下面我們看一下CFGR的配置:

*((uint32_t *)RCC_CFGR) = 0X00000400;

看到這裡,我覺得我們可以總結一下寫暫存器的基本套路:

  1. 檢視手冊相應章節,並瞭解縮寫意義,比如CFGR,全稱Clock Configuration Register,時鐘配置暫存器:

  2. 檢視暫存器定義:

  3. 檢視具體的某一位的定義,比如我們這裡設定為0X00000400,4是在後四位,所以重點關注低16bit,即從15 ~ 0bit:0000 0100 0000 0000,發現正好配置的PPRE1,值為100,手冊介紹如下:

用來配置APB1的從HCLK中獲得時鐘的分頻係數,二進位制100對應的分頻係數是2;HCLK是AHB匯流排的時鐘;然後AHB經過AHB-APB橋接將時鐘分配到APB1和APB2匯流排,APB1的匯流排對應PCLK1,APB2匯流排對應的PCLK2;這裡配置的就是APB1時鐘頻率的分頻值,為HCLK/2;

這裡有一個坑,雖然顯式的為PCLK1賦值了,但是其實隱式的同時將PCLK2(對應APB2),AHB都賦值了;只不過配置的是0;

比如PPRE2配置為000,對應的就是不分頻,或者說分頻數為1:

還有HPRE位,配置也是000,對應的不分頻,或者說頻數為1:

所以CFGR的配置重要的指定了AHB匯流排以及APB1和APB2匯流排的分頻/倍頻數;

設定PLL倍頻數

繼續看後面的程式碼,PLL是引數,即倍頻數,PLL上面已經介紹了,專門用於接入時鐘源然後後對其進行分頻倍頻再分配給各個匯流排(下面掛載的裝置):

PLL -= 2;
*((uint32_t *)RCC_CFGR) |= PLL<<18;  

那麼這裡為什麼要-2呢?我們先存疑,理解了下一行程式碼謎底就自然打開了;

PLL值左移18位,左移是位運算,左移+或運算 = 賦值操作,即將PLL值賦給18位起始的後面四位,通過檢視暫存器定義可以看到CFGR的18位起始到後面四位是PLLMUL,再檢視手冊對於PLLMUL的解釋:PLL的倍頻;不過看一下賦值情況0010(十進位制2)對應是4倍頻,0011(十進位制3)對應的是5倍頻,以此類推,就是暫存器的只是和真實的倍頻差2,看到這裡你就明白了為什麼在PLL -=2了:

緊隨其後的,可以知道是要給CFGR的第16bit賦值為1:

*((uint32_t *)RCC_CFGR) |= 1<<16;

檢視手冊,是描述PLL時鐘源,1對應是PLL的時鐘原始PREDIV1:

時鐘樹

關於PLLSRC以及PREDIV1他們之間關係在時鐘樹(Clock Tree)上面有比較明確的關係說明,從下圖可以看到作為時鐘源最開始是HSE,然後通過時鐘被分頻後成為了PREDIV1,然後再除以2,獲得了PLLSRC:

所以如果PLLMUL的值設定為1,即PLL的時鐘源採用內部高速時鐘(HSI),最終輸出時鐘訊號在HSI/2即可;
如果值設定為1即採用外部高速時鐘(HSE),在經歷了分頻後輸出為PREDIV1(時鐘)訊號,還需要再分頻一下,分頻係數為2;

FLASH ARC配置

再看下面的程式碼就沒有那麼怕了,直接明白,目的就是要給FLASH的ACR暫存器賦值:

*((uint32_t *)FLASH_ACR)|=0x2; 

不過這次要找FLash的ACR似乎並不那麼順利,很難直接從目錄中查詢到,需要全域性搜一下ACR,不過還好,沒有讓我們搜尋太多時間,在3.3.3章節中找到了:

暫存器的定義如下:

關於16進位制轉2進位制補0

這裡其實有一個補0問題,就是對於32bit,0x2究竟是0x0000 0002,還是0x2000 0000呢?迴歸本源,這兩種表達形式哪一個是2呢?毫無疑問,是第一個;
所以這裡的0x2,切換到32bit二進位制表示就是:0000 0000 0000 0000 0000 0000 0000 0010;可以看到其實有效賦值是LATENCY,位的值是010,檢視暫存器定義:

代表了比例,系統時鐘比FLASH存取週期的值,010對應的2,即系統時間是Flash的兩倍,所以同步的時候,需要做兩個週期的延遲用以同步CPU和Flash之間時鐘;
為什麼需要做此配置呢?首先程式都是存放在FLASH裡面,CPU需要從FLASH裡面取出執行指令,所以通訊之前需要首先同步時鐘;那麼就需要知道Flash的時鐘和系統時鐘(CPU時鐘)之間的差別;又因為我們配置時鐘是72MHz,這裡010的時鐘範圍包含了72MHz;

使能PLL

下面是對於RCC_CR暫存器最後的配置:

*((uint32_t *)RCC_CR) |= 0x01000000;
while(!(*((uint32_t *)RCC_CR) >> 25));

還是套路:拆解0x0100 0000,只有高16位有有效值,低16位全零;高16位轉換為2進位制之後值為:0000 0001 0000 0000,所以我們看到對應操作的是PLLON位,PLLON位說明如下:

寫入1則代表開啟PLL,上面我們做了關於PLL的倍頻的配置,PLL時鐘源的配置,都是需要在PLL使能之後才會生效,在嵌入式開發中,所有的配置都是基於裝置/ 元件使能的前提下才會配置生效;
至於while語句和上面配置HSE使能的語句的等待READY的功效是一致的;可以通過檢視暫存器定義,25位是PLLRDY,while迴圈就是等待這一位置為1,是否需要擔心其他位有1從而影響迴圈判斷?大可不必,因為31~26都是Reserved,必然為0,唯一的有效位就是PLLRDY位。
可以看到在RCC初始化的程式碼中,每次配置一個使能都是需要通過while迴圈來確認配置成功了;

配置PLLCLK為系統時鐘

最後一部分程式碼,讓RCC_CFGR與0x00000002做或運算:

*((uint32_t *)RCC_CFGR) |= 0x00000002;
while(temp != 0x02)
{  
		temp = *((uint32_t *)RCC_CFGR) >> 2;
		temp &= 0x03;
}   

有效為發生在低16位,0000 0000 0000 0010,有效配置位是最後兩位,即SW位(Switch,切換系統時鐘之意),解釋如下,可以看到10代表使用PLL的輸出(PLLCLK)作為系統時鐘:

之後的while語句則是輪訓檢視使能配置是否生效;注意這裡的使能生效並沒有採用上面單句while迴圈的方式,而是採用賦值方式來進行的,就是因為會有其他位配置會影響判斷;
首先看一下SWS位說明:

賦值判斷的方式也是非常巧妙,首先是RCC_CFGR右移(>>)2位,擠掉了SW位,現在SWS(Switch Status)位在最後面,然後將其和0x03進行與運算,與運算的目的就是:0位清零,1位保留原值(或運算目的:0位維持原值不變,1位用於置1)。

所以,和temp進行與運算0x03的其實是30bit數(最後兩位已經通過右移2位擠掉了):0000 0000 0000 0000 0000 0000 0000 11,所以就是要取用SWS的位的值,當返回值為“10”的時候,即0x02,代表PLL已經被設定為系統時鐘,說明RCC_CFGR的配置已經生效。