本文摘自:
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);
}
}
分析代碼套路
不要慌,看到一堆大寫字符,符號,我們梳理一下程式結構,總體來講一般分為三個部分,以后即使我們碰到再復雜的檔案,比如同檔案參考其實也不過是這樣的三個部分:
-
型別定義;型別的定義決定了變數的長度;
-
定義宏,即定義常量,常量不好理解,通過宏來定義,給予一個有意義的宏定義,程式可理解性會更強;除此之外,如果多個地方使用同一個變數,通過使用宏定義,可以實作只修改一個地方(宏定義)即可實作所有的參考地方做修改;
-
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;

總線上面其他組件的地址都是這個基地址偏移(offset);所謂的偏移就是指基于某個值再加上指定值;比如我們說基地址是100,某個組件偏移量是60,那么組件的(起始)地址160;
就是我們來看一下下一個APB2的總線地址,是基于總線基址偏移0x10000;
define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
翻看Memory Map可以看到APB2總線的基址是0x4001 0000;所以我們的宏定義APB2PERIPH_BASE是PERIPH_BASE偏移0x10000就是從這個表格里面來的;

后面我們繼續看,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)

最后一個計算,是RCC的使能位,所謂的使能就是時鐘生效,因為在嵌入式系統里面為了省電,很多組件默認是不作業的,不過這個不作業不是不上電,而是不開啟時鐘,只有開啟時鐘的組件才會作業,沒有時鐘就處于休眠狀態;使能就是開啟時鐘,讓組件處于作業狀態:
#define RCC_APB2ENR (RCC+0x18)
0x18從哪里來的呢?就是從芯片手冊的章節定義來的,這里的Address就是RCC基地址偏移0x18:

關于位計算
然后我們來看一下位運算“|“:
*((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;

關于地址型別
最后,為什么前面會有一個呢?在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是不夠的,所以有高位和低位兩個暫存器來記錄配置項;

繼續手冊下面給了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”,則一般是狀態位,有硬體層面設定,軟體層面只能夠讀取:

配置PC13高低電平
按照這個思路,我們再來看一下GPIOC_ODR,代碼如下:
*((uint32_t *)GPIOC_ODR) |= 0x00002000;
上手冊:

可以看到,ODR全稱是output data register,輸出資料暫存器,暫存器資料分布如下,可以看到其中16~31位是保留位,保留位一般標記都是0:
其中通過與運算為ODR13位賦值是1(其他bit保持不變),這一步操作是一個寫操作,即設定PC13為高電平,
硬體原理圖
代碼的含義我們清楚了,都是根據芯片手冊來的配置的,那么為什么要做這些配置呢?這個就還是需要看一下LED的原理圖,如下圖所示,表示了兩個LED的電路圖,其中我們重點關注LED2(PWR是Power的縮寫,是指電源LED):

可以看到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節:

其中,系統架構圖如下,我們看到GPIOC在APB2的總線下面,到此我們知道了剛才在代碼中為什么要使能APB2總線了以及配置GPIOC的CRH來實作使能PC13,

呼吸燈實作
代碼最后一部分就是實作了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的暫存器定義嗎?

所以,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,那么就意味著每秒鐘將會回圈81024次,那么如果回圈次數是8*1024次,就可以認為是1秒鐘;這一個就是為什么while回圈指定次數可以作為定時器來用(后面我們專門有一個原始碼解讀片內外設定時器);
debug時鐘設定
那么我們的時鐘是多少呢?如果你是除錯模式,這個時鐘是通過目標選項(Options For Target)來進行配置,開心就好:

如果是直接燒錄到STM32的板子里面,默認使用的是HSI(High Speed Internal,內部高速時鐘),即8MHz;所以為了Debug的效果和燒錄之后的效果保持一致,最好是設定為一致的時鐘頻率,
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/402393.html
標籤:嵌入式
上一篇:Linux之Nginx入門
