STM32(1):點亮LED(上)


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

概述

今天我們的開啟了STM32開發的第一站:點亮LED,今天的內容包含了很多基礎的知識,也有一些勸退的意味,不過,如果你能夠扛得住這波攻勢的,我覺得你高嵌入式方面真的是“風骨清奇,可造之材”。

程序總覽

typedef unsigned short     int uint16_t;

typedef unsigned           int uint32_t;
#define     __IO    volatile

#define PERIPH_BASE           ((uint32_t)0x40000000)
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)

#define GPIOC_BASE            (APB2PERIPH_BASE + 0x1000)
#define GPIOC                   (GPIOC_BASE)
#define GPIOC_CRH                           (GPIOC+0x04)
#define GPIOC_ODR                           (GPIOC+0x0C)

#define AHBPERIPH_BASE        (PERIPH_BASE + 0x20000)
#define RCC_BASE              (AHBPERIPH_BASE + 0x1000)
#define RCC                     (RCC_BASE)
#define RCC_APB2ENR                     (RCC+0x18)
#define RCC_CR                              (RCC+0x00)
#define RCC_CFGR                            (RCC+0x04)

#define FLASH_R_BASE          (AHBPERIPH_BASE + 0x2000)
#define FLASH                   (FLASH_R_BASE)
#define FLASH_ACR             (FLASH+0x00)

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;
	}   
}

void delay(unsigned int time)
{    
	 unsigned int i=0;  
	 while(time--)
	 {
			i=10000000;
			while(i--) ;    
	 }
}


int main(void)
{
	*((uint32_t *)RCC_APB2ENR)  |= 0x00000010;

	*((uint32_t *)GPIOC_CRH)        |= 0x00300000;
	*((uint32_t *)GPIOC_ODR)        |= 0x00002000;

	while(1)
	{
			*((uint32_t *)GPIOC_ODR)    &= ~(1<<13);
			*((uint32_t *)GPIOC_ODR)    |= 0x00000000;

			delay(3);

			*((unsigned int *)GPIOC_ODR) &= ~(1<<13);
			*((unsigned int *)GPIOC_ODR) |= 0x00002000;

			delay(1);
	}
}

分析代碼套路

不要慌,看到一堆大寫字符,符號,我們梳理一下程序結構,總體來講一般分為三個部分,以后即使我們碰到再復雜的文件,比如同文件引用其實也不過是這樣的三個部分:

  1. 類型定義;類型的定義決定了變量的長度;

  2. 定義宏,即定義常量,常量不好理解,通過宏來定義,給予一個有意義的宏定義,程序可理解性會更強;除此之外,如果多個地方使用同一個變量,通過使用宏定義,可以實現只修改一個地方(宏定義)即可實現所有的引用地方做修改;

  3. main函數,程序運行要走的函數;

    // 1.類型定義
    typedef unsigned short int uint16_t;
    ... ...

    // 2. 定義宏

    define __IO volatile

    define PERIPH_BASE ((uint32_t)0x40000000)

    define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)

    ... ...

    // 3. 主函數
    int main(void)
    {
    ... ...
    delay(3);
    ... ...
    }

    // 4.調用函數
    void delay(unsigned int time)
    {
    ... ...
    }

任何程序基本都是這三部分的延伸和拓展。
下面我們來看主函數,在研究主函數的時候,上面我們定義的宏自然就明白了;

使能APB2總線

下面是第一行代碼,這一行代碼的含義是向目標內存地址寄存器地址中通過與計算0x00000010;至於RCC_APB2ENR是做什么的我們放在后面來講解,首先搞清楚它的計算脈絡;
*((uint32_t *)RCC_APB2ENR) |= 0x00000010;

地址計算

首先搞清楚RCC_APB2ENR是什么東西,首先追溯一下它的define計算過程

#define PERIPH_BASE           ((uint32_t)0x40000000)
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)
... ...
#define AHBPERIPH_BASE        (PERIPH_BASE + 0x20000)
#define RCC_BASE              (AHBPERIPH_BASE + 0x1000)
#define RCC                 	(RCC_BASE)
#define RCC_APB2ENR		(RCC+0x18)

PERIPH_BASE是總線基地址,什么是基地址?就是某個組件的起始地址,因為分配各組件的地址是一個范圍,所以有起始地址和結束地址,基地址就是指起始地址,但是並不是所有組件的起始地址都叫做基地址,只有那些組件下面還要掛在其他組件,即其他組件的地址是基於它計算出來的地址才叫做基地址;



0x40000000從哪里來的呢?翻看芯片手冊“3.3 Memory map”章節里面,這里會羅列各個STM32組件的內存地址映射,注意,是內存地址映射,並不是真正的內存地址,為了訪問這些組件需要構造專門為這些組件分配一定的虛擬內存地址空間,這些虛擬的內存地址並不會真正的和物理內存做映射,而是和指定的寄存器做映射;
表格排列的地址順序從高地址到低地址,所以需要拉倒最下面看到基地址,拖拽到表格的最下面就可以看到BUS的起始地址,即總線的基地址是0x4000 000;


file
總線上面其他組件的地址都是這個基地址偏移(offset);所謂的偏移就是指基於某個值再加上指定值;比如我們說基地址是100,某個組件偏移量是60,那么組件的(起始)地址160;
就是我們來看一下下一個APB2的總線地址,是基於總線基址偏移0x10000;

define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)




翻看Memory Map可以看到APB2總線的基址是0x4001 0000;所以我們的宏定義APB2PERIPH_BASE是PERIPH_BASE偏移0x10000就是從這個表格里面來的;


file
后面我們繼續看,AHBPERIPH_BASE,即AHB總線上面基地址,注意AHB總線上面橫跨了幾個地址范圍0x5x,0x4002x,0x4001x;而我們所要獲取的RCC的是0x4002地址段的,所以在計算基地址的時候,就不再是直接看最后一個,因為最后一個是0x4001x地址段;



我們是通過總線基址+0x20000來進行指定的,所以具體的組件的基址的計算也是靈活的,是需要看你要組件所在的地址段來進行偏移計算的;然后是RCC_BASE,也是根據RCC的起始地址0x4002 10000,所以基於AHBPERIPH_BASE(0x4002 0000)基礎上再偏移0x10000,此時得到了RCC的基址:0x4002 10000;

#define AHBPERIPH_BASE        (PERIPH_BASE + 0x20000)
#define RCC_BASE              (AHBPERIPH_BASE + 0x1000)
#define RCC                   (RCC_BASE)

file

最后一個計算,是RCC的使能位,所謂的使能就是時鍾生效,因為在嵌入式系統里面為了省電,很多組件默認是不工作的,不過這個不工作不是不上電,而是不開啟時鍾,只有開啟時鍾的組件才會工作,沒有時鍾就處於休眠狀態;使能就是開啟時鍾,讓組件處於工作狀態:

#define RCC_APB2ENR		(RCC+0x18)

0x18從哪里來的呢?就是從芯片手冊的章節定義來的,這里的Address就是RCC基地址偏移0x18:
file

關於位計算

然后我們來看一下位運算“|“:

*((uint32_t *)RCC_APB2ENR)	|= 0x00000010;

在C語言里面有七種運算:

// 1.賦值運算,即將具體的值賦給一個變量:
int a = 5;
// 2.算術運算,即+-*÷四則運算:
int c = b + 5;
// 3.邏輯運算,邏輯運算的結果是true/ false,運算包括與運算(“&&”),或運算("||"),取反運算(“!"):
if(a && b){
		... ...
}
int d = c || b;
// 4.關系運算符,包括>,<.<=,>=,!=,==,關系運算的結果也是true/ false
if(a == b){
		... ...
}
// 5.三目運算
int e = a < b ? d : e;
// 6. 位運算,這個重點,也是我們做地址運算普遍采用的運算方式,
// 包括與運算&, 或運算!,異或^,左移<<,右移>>;

// 7.單目運算,++,--,~(取反操作)
for(int i = 0; i<MAX; i++){
		... ...
}

那么這里為什么采用或運算呢?首先搞清楚什么是位運算,位運算和其他運算最大區別在於其他的運算都是以數據類型為單位進行設計規則,而位運算不再是關注數據類型作為一個整體,而是基於每一個bit來設計運算規則;
然再搞清楚什么是或運算,x|y,x和y只要有一個為0(false)就是0,x和y只有都是1(true)才是1(true);


使能PC端口

代碼中要做的事情是使能APB2總線,首先是查找手冊,定位到RCC_APB2ENR;


在芯片手冊的register小節中,將會非常詳細羅列出RCC_APB2ENR這個引腳所對應寄存器的每一位的含義;我們可以把每一個引腳理解為寄存器,一個32bit的寄存器,引腳可以抽象為輸入/輸出接口,用於“存放”輸入/輸出的數據;



這里我們目標是確保設置PC為使能狀態即1,因為采用的或運算,與值為0的位置維持原來的值,而與值為1的位(IOPCEN,第4位)設置為1,於是從32位到0位(注意表示是大端表示方式,從高位到低位),依次是:0000 0000 0000 0000 0000 0000 0000 0000 0001 0000,轉化為16進制就是0x00000010;
file

關於地址類型

最后,為什么前面會有一個呢?在C語言里面變量大體有兩種分類,一種是值,一種是(內存)地址,比如整型2009,可以是代表值2009,也可以代表要訪問某個內存地址,怎么代表要訪問是這個地址呢?就是在前面添加一個“”;
其實你想沒想過當你定義個變量的時候,所謂的初始化,其實就是為這個變量和一個內存地址做了綁定,這種綁定是記錄在變量定義表的,找到了地址之后,對這個地址進行賦值;
所以,當你看到下面的代碼:

int a = 5;

其實本質是下面的形式,其中a經過初始化,將變量a和地址0x40000230(這個地址是舉例)進行綁定:

*0x40000230 = 6;

配置PC13

繼續看配置GPIOC_CRH的代碼:

*((uint32_t *)GPIOC_CRH) |= 0x00300000;

什么是GPIOC_CRH?上手冊,在第9章介紹GPIO和AFIO的GPIO寄存器章節里面可以看到GPIOx_CRH,全稱是Config Register High,即高位配置寄存器,有高位就有地位,看來配置項很多,一個32bit是不夠的,所以有高位和低位兩個寄存器來記錄配置項;


file
繼續手冊下面給了GPIOx_CRH的32bit每個bit的含義,GPIOx中的x是指A,B,C,我們在STM32的板子上面都可以看到PAx,PBx,PCx的字樣(x的取值范圍就是1~16),P代表Port即端口(端子),ABC是分類;CRH描述的是PA/PB/PC的第9引腳到16引腳;我們這里是要設置PC13的相關配置;



其中CNF位配置的是該引腳是作用方向,是輸入還是輸出;MODE位則是配置輸出的最大時鍾頻率(如果是輸出的話基本可以忽略MODE了);注意,每個CNF占兩個bit位,每個MODE也是占兩個bit位;其中rw代表這個bit位是可以通過軟件來進行讀寫,如果碰到了有的位是“r”,則一般是狀態位,有硬件層面設置,軟件層面只能夠讀取:


file

配置PC13高低電平

按照這個思路,我們再來看一下GPIOC_ODR,代碼如下:

*((uint32_t *)GPIOC_ODR) |= 0x00002000;

上手冊:


file


可以看到,ODR全稱是output data register,輸出數據寄存器,寄存器數據分布如下,可以看到其中16~31位是保留位,保留位一般標記都是0:



其中通過與運算為ODR13位賦值是1(其他bit保持不變),這一步操作是一個寫操作,即設置PC13為高電平。


硬件原理圖

代碼的含義我們清楚了,都是根據芯片手冊來的配置的,那么為什么要做這些配置呢?這個就還是需要看一下LED的原理圖,如下圖所示,表示了兩個LED的電路圖,其中我們重點關注LED2(PWR是Power的縮寫,是指電源LED):
file

可以看到LED2兩端一個接着的是VCC,即3.3v,另外一端接的是PC13端口;當PC13是高電平的時候LED2兩端沒有電壓差,所以LED2沒有通電,所以處於關燈狀態;如果PC13是低電平狀態,LED2兩端有電壓差,所以處於通電狀態,此時LED2將會亮燈;


所以可以通過設置PC13的低電平和高電平才讓LED2電量和關閉;設置PC13的高低電平就是設置GPIOC寄存器的ODR的值,1即為高電平,燈滅,0為低電平,燈亮。



上面是LED2的原理圖,我們知道了通過PC13來控制LED2的亮滅;但是PC13並不是直接就可以控制的,在嵌入式開發領域通常為了節能,只有需要某個端口的時候,才需要上電,上時鍾,只有有時鍾輸出才能夠工作;否則端口的時鍾關閉,你設置任何值對於對於端口來說都是無效的;
所以如果我們想要讓對於PC13的控制生效,就需要使能PC的端口使能;要設置使能首先就要明白所有的片內外設都是掛在總線上面,我們需要通過打開總線時鍾來實現對於端口的使能,那么GPIOC掛在哪個總線下面呢?上手冊,打開3.1節:
file
其中,系統架構圖如下,我們看到GPIOC在APB2的總線下面,到此我們知道了剛才在代碼中為什么要使能APB2總線了以及配置GPIOC的CRH來實現使能PC13。
file

呼吸燈實現

代碼最后一部分就是實現了while循環,通過亮-滅-亮-滅-...從而實現呼吸燈的效果:

while(1)
{
				*((uint32_t *)GPIOC_ODR) &= ~(1<<13);
				*((uint32_t *)GPIOC_ODR) |= 0x00000000;
				delay(3);
				*((unsigned int *)GPIOC_ODR) &= ~(1<<13);
				*((unsigned int *)GPIOC_ODR) |= 0x00002000;

				delay(1);
}

亮燈

首先我們杠一下while里面的第一行,這里面運算比較復雜:

*((uint32_t *)GPIOC_ODR) &= ~(1<<13);

首先我們來拆解一下1左移13位,左移操作屬於我們上面提到的位運算,位運算特點就是和整體數值無關,只是針對每個bit位來盡心運算;1左移13位,就是14位,添加2兩位湊成16位(湊成2的n次方值)即:0010 0000 0000 0000,外面的“~”是代表取反,取反是一個單目運算,即針對操作數本身的操作,取反之后的值:1101 1111 1111 1111;



取反之后的值和GPIOC_ODR的原始值進行與運算(&),與運算的規則就是只有兩個位操作數都是1(true)結果才是1(true),否則結果就是0(false);
和還記得GPIOx_ODR的寄存器定義嗎?



file
所以,GPIOC_ODR和1101 1111 1111 1111做與運算,目的就是如果之前是1的還是1,之前如果是0的還是0;但是對於ODR13而言,無論之前值是什么,此番設置之后就是0了。這里有一點注意,3116為保留位,真正參與位運算的是150位,所以真正與運算的值是0000 0000 0000 0000 1101 1111 1111 1111;



然后GPIOC_ODR再和0x00000000做或運算,操作數和0x00000000或運算實現了原來是1的保持,原來是0的保持0;這一步其實意義並不大,可以是處於對稱的目的做的這一步操作;ODR經過了上述的與運算和或運算之后,將ODR設置為0(清零),從而讓LED產生電壓差,實現了亮燈效果;

滅燈

類似,第6行和第7行代碼如下:

*((unsigned int *)GPIOC_ODR) &= ~(1<<13);
*((unsigned int *)GPIOC_ODR) |= 0x00002000;

第一行代碼含義和上面介紹的完全一致,目的是用於維持其他位不變只是針對第13位ODR13,設置其為0,即“清零”操作;



然后第二行GPIOC_ODR和0x00002000做與運算,前面4個零代表32~16位保留位,可以忽略,重點關注2000,轉換為16進制是0010 0000 0000 0000,即實現設置ODR13的位的值為1;設置ODR13為1的效果,讓LED兩端沒有了電壓差(PC13為1即高電平,高電平即3.3V),於是有了滅燈的效果;


呼吸燈小節

所以設置ODR的狀態一般都是兩個步驟:
第一步是ODR13清零;
第二步是ODR13設置為目標值;不過在設置目標值為0的場景下,這一步似乎沒有什么價值;不過處於對稱的目的,還是會設置一下;於是有了*((unsigned int *)GPIOC_ODR) |= 0x00000000;
基於上述的計算,你會發現,或運算一般用於設定指定位的值(而不影響其他位);與運算用於“清零”(保持指定位不變);

延時函數delay

我們還需要注意在亮滅之間還有一個delay的函數:

void delay(unsigned int time)
{    
	 unsigned int i=0;  
	 while(time--)
	 {
			i=10000000;
			while(i--) ;    
	 }
}

delay這個函數就是一個while循環,達到指定次數之后就退出,從而實現了延時的效果,類似c/ Java里面的Sleep函數的效果;那么為什么能夠實現這個效果呢?這個是因為任何芯片都有一個時鍾的概念,比如我們說STM32的APB2總線是48MHz,其實講述的就是APB32每秒鍾會經歷481024次時鍾;所以在單位時間內的時鍾/周期次數,就是頻率(也稱之為時鍾頻率)的概念;
再回到上面的例子中,如果我們設置了時鍾是8MHz,那么就意味着每秒鍾將會循環8
1024次,那么如果循環次數是8*1024次,就可以認為是1秒鍾;這一個就是為什么while循環指定次數可以作為定時器來用(后面我們專門有一個源碼解讀片內外設定時器);


debug時鍾設置

那么我們的時鍾是多少呢?如果你是調試模式,這個時鍾是通過目標選項(Options For Target)來進行配置,開心就好:
file
如果是直接燒錄到STM32的板子里面,默認使用的是HSI(High Speed Internal,內部高速時鍾),即8MHz;所以為了Debug的效果和燒錄之后的效果保持一致,最好是設置為一致的時鍾頻率。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM