學習stm32有一個多月了,現在開始整理下思路,最大的感受就是,看起來是在學ARM,實際上是在補51,或者說,學習ARM之前真應該好好學學單片機。或者我根本就不應該分這么清楚,學就是了。都說學過了單片機再學ARM就很容易了,自以為單片機學的不錯,想當然的認為ARM就不那么難了,剛開始的時候確實是這么想的,買了開發板,打開做好的工程,修修改改,從點燈開始唄,也沒覺得有什么難的,不求甚解,達到目的就行了。過了幾天,深入的研究下,仔細看下庫函數什么的就不明白了,更不用談*.s的啟動文件了,工程下面那么多文件夾也不知道是什么意思,要自己從0寫的話根本無從下手,突然覺得有了51的基礎怎么學起來還這么難。難道別個說錯了?直到看到這篇帖子:從51到ARM這路怎么走?才恍然大悟,當年學51的時候,寫個100多行的程序用矩陣鍵盤和LCD做個密碼鎖,覺得還行,然后就一直停在那個階段了,沒什么進步,現在看來只是掌握了最基本的開發流程,並沒有真正懂51。一直覺得自己不會用到匯編,所以一遇到匯編就skip,至於c語言,也是一知半解。總之,以前欠的太多了,現在只有補唄。
就從點燈開始,用庫做就是這樣
Step1 包含*.h頭文件
#include "stm32f10x.h"
Step2 定義GPIO初始化結構體
GPIO_InitTypeDef GPIO_InitStructure;
Step3 先定義LED的亮滅,我的芯片是STM32F103VET6,LED接的是PB5。
#define LED_ON GPIO_SetBits(GPIOB, GPIO_Pin_5);
Step4 各種聲明,時鍾配置、LED配置和延時函數。
void RCC_Configuration(void);
void LED_Config(void);
Step5 各種配置,初始化函數。
//系統時鍾配置為72MHZ
void RCC_Configuration(void)
{
SystemInit();
}
//LED 控制初始化函數
void LED_Config(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB| , ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
Step6 終於進main()函數了。
Int main(void)
{
RCC_Configuration(); //系統時鍾配置
LED_Config(); //LED控制配置
LED_ON; //LED1亮
while (1)
{
}
}
開發板接好線,編譯成功之后load,OK!Led閃爍了。是不是認為做完了,該收工了。其實還沒開始呢。分析下每步到底是什么意思,為什么要這樣寫。
Step1 中為什么要包含#include "stm32f10x.h"這樣的一個頭文件,有什么用?
Stm32f10x.h是控制器專用頭文件,包含了STM32F10X全系列所有外設寄存器的定義(寄存器的基地址和布局)、位定義、中斷向量表、存儲空間的地址映射等。所以要包含這個文件。
Step2 GPIO_InitTypeDef GPIO_InitStructure;這是什么意思?
GPIO_InitTypeDef是自己定義的一個數據類型(stm32f10x.h):
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
利用關鍵字typedef和struct,自定義了一個結構體變量,結構體變量的類型名就是GPIO_TypeDef,然后利用這個自定義的數據類型定義了名為GPIO_InitStructure的結構體變量,這個變量在LED 控制初始化函數中將會用到。
Step3中是這樣定義led的亮:
#define LED_ON GPIO_SetBits(GPIOB, GPIO_Pin_5);
看下這個函數(在stm32f10x_gpio.c中):
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_Pin));
GPIOx->BSRR = GPIO_Pin;
}
傳遞了兩個參數:GPIOx和GPIO_Pin,對應着上面的GPIOB, GPIO_Pin_5, assert_param()這個函數是檢查輸入參數是否有效,如果無效就產生異常;如果有效,帶入參數,執行完GPIOx->BSRR = GPIO_Pin之后就是把PB5設置為1。具體是怎么操作的?先看兩個參數的類型,GPIOx是指向GPIO_TypeDef這個自定義結構體類型的指針,類型為指針,指向一個自定義的結構體(在stm32f10x.h中):
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
結構體的類型為自己定義的GPIO_TypeDef。其中__IO和uint32_t是什么東東?
__IO定義在這里(core_cm3.h):
#define __IO volatile
這個宏定義就是用__IO來替換volatile的,那volatile這個關鍵字又有什么作用。Volatile是不穩定的意思,說明這種類型的值容易發生變化,使用volatile就是不讓編譯器進行優化,每次讀取或者修改值的時候,都必須通過它的硬件地址重新讀取或修改,如果不加volatile關鍵字,編譯器就會進行優化,把定義的常量保存在寄存器中,以后就通過訪問這個寄存器來訪問這個值,因為訪問寄存器的速度比訪問內存還要快,就達到了優化的效果,但是這個值可能會在外部發生改變,尤其是在嵌入式與硬件相關或者中斷的這些地方,它的值在外部改變了之后,裝在寄存器中的那個值並沒有隨着更新,依舊取的是之前優化的時候存下的值,所以取到的就是錯誤的值了。
__IO:輸入輸出口,作為輸入的時候,當然不能進行優化,它的值隨時都會改變,作為輸出的時候;也不能優化,優化之后,輸出的始終是同一個值,而這個值是會改變的,如果連續兩次輸出相同值,編譯器認為沒改變,就會忽略后面那次輸出,這就很嚴重了。
uint32_t定義在這里(stdin.h):
typedef unsigned int uint32_t;
使用typedef定給unsigned int 類型氣的別名,因為不同的平台會有不同的字長,所以利用預編譯和typedef可以最有效的維護代碼。這樣很明顯的看出是4個字節。
好了,另一個參數自然也就明白了,是2字節的變量了。
再回到這個函數:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
由於GPIO是個指針,GPIO_Pin為常量,所以
#define LED1_ON GPIO_SetBits(GPIOB, GPIO_Pin_5); 預編譯之后相當於
GPIOB->BSRR = GPIO_Pin_5;就是把GPIOB的BSRR寄存器設置為0x0020,剛好 第五位置為1,由於是指針變量,所以用“->”直接取自定義結構體GPIOB 中的BSRR 變量的地址,然后賦值為GPIO_Pin_5,在stm32f10x_gpio.c中有這樣的定義:
#define GPIO_Pin_5 ((uint16_t)0x0020)
0x0020為0x 0000 0000 0010 0000,然后查STM32參考手冊:

把GPIOB的BSRR寄存器的第五位置1,這個寄存器為端口為設置/清除寄存器。
Step4 聲明這個文件需要用到的函數,時鍾、LED。
Step5
首先是時鍾配置,只需要調用系統初始化函數void SystemInit (void)就OK了,在system_stm32f10x.c中,這是微控制器專用系統文件,而函數SystemInit用來初始化為控制器。
然后是LED控制初始化函數,使能APB2口中GPIOB的時鍾,設置為通用推挽輸出模式,最大輸出速度為50MHz,然后向PB5口送數據,調用GPIO_Int函數,OK。(至於推挽模式和最大輸出速度為什么這樣設置,現在還不是很清楚,研究中···)
Step6 進入main函數,時鍾和led配置完了就停在while(1)中,成功點亮led。
現在不用庫做,直接操作寄存器。
Step1 定義變量
#define RCC_APB2ENR *(volatile unsigned long *)0x40021018
#define GPIOB_CRL *(volatile unsigned long *)0x40010C00
#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C
Step2 進入main()函數
void main(void)
{
RCC_APB2ENR |= 1<<3;
GPIOB_CRL = (2<<20) | (0<<22);
GPIOB_ODR = 1<<5;
while (1)
{
}
}
保存,編譯,load,OK!點亮led。
然后分析下為什么這樣做:
Step1 要操作PB5口,首先要使能PB口的CLK,這是stm32特殊的地方,上電時默認外設時鍾都是關的;然后是設置PB口的控制寄存器;最后就是送數據。
GPIOB的時鍾通過APB2連接,所以應該設置RCC_APB2ENR,查參考手冊:

第三位就是PB的時鍾使能。然后就是找這個寄存器的地址了。再查memory mapping(stm32f103ve.pdf)
基地址就是0x4002 1000,偏移地址:0x18,所以RCC_APB2ENR地址為基地址+偏移 地址:0x4002 1000 + 0x18 = 0x4002 1018。
還可以去查keil給出的頭文件\Keil\ARM\INC\ST\STM32F10x\stm32f10x_map.h
#define PERIPH_BASE ((u32)0x40000000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)
加上偏移0x18,則RCC_APB2ENR的地址為:
0x4000 0000 + 0x2 0000 + 0x1000 + 0x18 = 0x4002 1018
於是就這樣定義了:
#define RCC_APB2ENR *(volatile unsigned long *)0x40021018
0x40021018只是個值,(volatile unsigned long *)進行強制轉換,說明這個值是個地址,類型是unsigned long,意思是,讀寫這個地址時,寫入和讀出的都是unsigned long類型。加了volatile確保不被編譯器優化,每次直接讀值。
(volatile unsigned long *)0x40021018是一個指針,不會變,但里面的值容易變,再在前面加“*”,則可以直接操作這個指針指向的地址里面的值,然后就可以直接對這個內存進行讀寫操作。
剩下的兩個用同樣的方法找到地址然后定義。
Step2 main()函數
RCC_APB2ENR |= 1<<3;查手冊BIT3就是PBEN時鍾使能位,置“1”,其他位不管。
GPIOB_CRL = (3<<20) | (0<<22); 將PB5設置為輸出,看手冊得出 MODE5 是
bit 20 21 控制的,CNF5 是bit 22 23,MODE5應該設置 10(0x2) 選擇 2MHZ 輸出,CNF5 選擇00(0x0),通用推挽模式,於是將這個值寫入。
GPIOB_ODR = 1<<5;在相應的ODR為寫“1”。
OK!點亮之后停在while(1)中。
好吧,點個燈花了半個月,寫個總結又花了2天,真是了解和做出來不是一回事,做出來和寫出來也不是一回事,就算寫出來了也未必理解的是對的。現在差不多知道該怎么學了,突然發現還有好多東西要學。
網上的資源真是太好了,向這些作者表示感謝:
從51到arm:
http://www.ourdev.cn/thread-5462507-1-1.html
__I、 __O 、__IO是什么意思?
http://blog.csdn.net/denghuanhuandeng/article/details/7281293
uint8_t / uint16_t / uint32_t /uint64_t 是什么數據類型:
http://blog.csdn.net/kiddy19850221/article/details/6655066
*(volatile unsigned long *)的理解:
http://hi.baidu.com/lrbpp/blog/item/8f796d207cded797d0a2d3aa.html
