STM32的芯片架構
以STM32F103ZET6爲例
STM32芯片主要由內核和片上外設組成,其中內核是由ARM公司設計的,例如Cortex-M3內核,內核和外設的關係就好比電腦的處理器與顯卡,內存,硬盤關係,常用的如GPIO,USART,IIC,ADC,DAC等都屬於片上外設,這些片上外設都是由ST公司設計的,ARM公司只負責對芯片技術進行授權。
詳細圖:
存儲器映射與寄存器映射
芯片上各種功能部件被排列在一個4GB的空間中,這個4GB被分成8個區域,也就是被分成8個存儲器,每個區域各有512kb的存儲空間,這些區域被分成代碼區,SRAM區,外設區等等
每一個存儲器本身並沒有地址,給每個存儲器分配地址的過程叫做存儲器映射,這是由生產廠商分配的,而給存儲器重新分配地址叫做存儲器的重映射
每一個存儲器裏面由各種外設各種寄存器,給寄存器分配地址的過程叫做寄存器映射,重新分配地址叫做寄存器的重映射
簡單做了個圖幫助理解:
總線基地址
片上外設區有三條總線,根據速度不同分位APB1、APB2、AHB總線,總線的最低地址就是總線的基地址,同時也是掛載在該總線上的首個外設的地址,由於APB1總線的地址最低,各種外設的地址從這裏開始,所以APB1的地址也叫外設基地址
總線名稱 | 總線基地址 | 相對外設基地址的偏移 |
---|---|---|
APB1 | 0x4000 0000 | 0x0 |
APB2 | 0x4001 0000 | 0x0001 0000 |
AHB | 0x4001 8000 | 0x0001 8000 |
外設基地址
總線上有各種外設,每個外設也有自己的地址範圍,前面的圖也有體現,外設的首個地址也叫外設基地址,這裏以GPIO爲例
外設名稱 | 外設基地址 | 相對 APB2 總線的地址偏移 |
---|---|---|
GPIOA | 0x4001 0800 | 0x0000 0800 |
GPIOB | 0x4001 0C00 | 0x0000 0C00 |
GPIOC | 0x4001 1000 | 0x0000 1000 |
GPIOD | 0x4001 1400 | 0x0000 1400 |
GPIOE | 0x4001 1800 | 0x0000 1800 |
GPIOF | 0x4001 1C00 | 0x0000 1C00 |
GPIOG | 0x4001 2000 | 0x0000 2000 |
如何操作單片機上各種各樣的寄存器?固件庫開發的簡介
首先這裏總結一下,想要找到一個寄存器的步驟:
舉個栗子,我想找到GPIOE上ODR寄存器的位置要怎麼辦呢?
- 打開STM32參考手冊,找到寄存器所在總線
比如這裏找到的GPIOE的外設地址是 0x4001 1800
- 接着可以去找手冊上對應GPIO外設上ODR寄存器的偏移地址
這裏ODR寄存器的偏移地址是0x0C
- 操作GPIOE的ODR寄存器的地址是
0x4001 1800+0x0C=0x4001 180C
知道地址後要怎麼操作呢?
學過C語言我們知道,指針就是來操作地址的那麼是否可以用將得到的數值強制轉化成指針再進而操作對應地址的寄存器呢?當然可以
首先要把得到的數值轉化成地址,用C語言的強制轉化操作
(unsigned int *)0x4001180C
這時候得到的就是纔是寄存器的地址,得到地址後還不能操作,因爲這只是個地址,還不是一個寄存器本身,要想操作地址對應的寄存器,還要再用一個指針的知識,在前面加個*號
*(unsigned int *)0x4001180C
這樣得到的就是一個可以操作的寄存器,由於STM32是32位的,所以每個寄存器都是由32個bit組成,我們現在用這樣一段代碼給GPIOE ODR寄存器第6個位寫入1
*(unsigned int *)0x4001180C |= ( 1 << 5 );
用位操作可以增強程序的可讀性
但是每次都這樣操作未免太過於麻煩,如果能將這麼複雜的一大串數字用一個一段可讀性強的字符串表示那不是更好?這時候宏定義的作用就體現出來了
#define GPIOE_ODR *(unsigned int *)0x4001180C
GPIOE_ODR |= ( 1 << 5 );
用宏定義給原本複雜的寄存器地址一個可讀的新名字,程序的可讀性更強,一看就知道你操作的是什麼寄存器,這一步類比51單片機,起到了sbit的作用,只不過這個地址名沒有用類似reg51.h的頭文件進行封裝,雖然這樣方便了很多,但是細心去看參考手冊就知道,一個外設對應了可不止一個寄存器,例如GPIO這個外設就有ODR、IDR、CRH、CRL、BRR等等寄存器,每一次都這樣定義豈不是很麻煩?有沒有什麼辦法解決呢?這時候就要用到C語言中的結構體了,細心一點你就會發現,結構體上每兩個變量相鄰變量間的地址之差剛好是4,同時外設上每兩個相鄰的寄存器間的地址之差也是4,那麼這時候只要在結構體中按順序定義外設上各個寄存器,就可以用結構體來儲存每個寄存器的配置了
typedef unsigned int uint32_t;
typedef struct //對於32位的單片機,結構體每一個成員的位置偏移量爲4
{
uint32_t CRL;
uint32_t CRH;
uint32_t IDR;
uint32_t ODR;
uint32_t BSRR;
uint32_t BRR;
uint32_t LCKR;
}GPIO_TypeDef;
得到這樣一個結構體後我們知道當你新建一個結構體變量,其地址是隨機分配的,如果要將其指向一個特殊地址,比如指向GPIOE的外設基地址,就得用宏定義進行強制轉化,但是在這之前我們可以定義一下外設基地址,總線基地址,以及特定外設的基地址,再通過相加減得到所需要的寄存器的地址,事實上ST官方的固件庫就是這樣做的
#define PERIPH_BASE ((unsigned int)0x40000000) //外設基地址
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000) //APB2總線基地址
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800) //GPIOE基地址
#define GPIOE ((GPIO_TypeDef *)GPIOE_BASE) //把GPIOB_BASE 強制轉換成 GPIO指針
這樣假如你要操作GPIOE上的CRL寄存器就可以這樣操作
GPIOE->CRL = xxxxx;
方便了許多
後面我們還可以再定義一個函數用於操作所需要操作的寄存器,這時候只需要填入參數就可以實現運用函數來操作配置各種寄存器或者各種外設的功能了,這也就是固件庫實現的基本思想。
這是固件庫操作GPIO外設的一個函數,可以簡單參考一下
void GPIO_SetBits (GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) //GPIO_Pin 每一位對應爲1 已經在頭文件中聲明
{
GPIOx->BSRR |= GPIO_Pin; //設置對應的ODRy位爲1
}