在進行底層開發時,尤其是C語言,我們時常與暫存器打交道,不過到底暫存器的確切定義是甚麼?有時很難確切定義
有些書將暫存器想像成一排書櫃中的特定一格,對這些特殊抽屜,可以將抽屜打開拿取裡面的紙條,也可以把新的紙條放進去。我個人蠻喜歡這個比喻法,但也讓我思考,到底能不能用更精準的方式去定義暫存器呢
為了釐清暫存器的概念,我特地找了一塊32bits的STM32F4型開發版,核心使用STM429IGT6,其實我們編寫程式就是在控制這顆CPU的眾多引腳來達到特定需求,例如傳感器的輸入經過運算後,經由GPIO引腳輸出控制
我們可以藉由控制引腳的輸出以及輸入來達到特定目,。而開發版上的引腳都被分配了一組獨一無二的地址位置,透過更改這些地址儲存的數值,就可以有效的控制引腳要怎麼輸出、如何輸出。因此我們可以把引腳當作控制的最基本單位,而暫存器就是引腳背後的控制原理
下圖是這次使用的STM32開發版引腳圖,它擁有176個引腳
其實記憶體本身是不具有地址概念的,所謂的地址是由芯片廠商或用戶自行規劃出來的,也就是說地址的概念其實是我們抽象出來的
那重點來了,要如何知道虛擬地址的範圍是多大?
為了方便理解,我們從STM官方網站下載相應的data sheet(我的開發版使用STMF429),從下圖可以看出記憶體的映射圖範圍為0x0000 0000
~0xFFFF FFFF
,總共有4294967296個,也就是4G大小的空間。請注意4G大小並不代表核心版的真實儲存大小,而是核心版有能力表示這麼大的空間,這兩者是有差別的
4G = 4294967296 = 2^32,簡單來說就是處理器的位元數的次方數。STM32F429這個開發版的核心處理器為32位元,處理器裡有很多很多負責存儲數據的暫存器,而這些暫存器的長度範圍恰好是32bits
我們假設一個長度為32bits的暫存器,它儲存了一個整數型態的數據,數值為4,將4轉換成二進位制等於0000 0000 0000 0000 0000 0000 0000 0100
,我們分別把這一連串二進位數值存放到0x0000 0000
~0x0000 001F
的地址空間中,這一塊32bits長度的連續空間就稱為暫存器
由此可知每一個暫存器的起始地址之間存在32bits(4bytes)的差距,起始位置是0x0000 0000
,最大值是0xFFFF FFFF
,這個範圍建構了4G大小的尋址空間,官方網站的Memory Mapping就是這麼計算出來的。所以我們把這個位記憶體分配空間的行為稱為記憶體映射
為已經記憶體映射完的記憶體地址命名的過程就稱為暫存器映射
暫存器映射的目的在於編寫程式時可以用定義好的暫存器名進行操作,而不用每次都調用難懂的16進制,例如下面的這段程式
/* GPIOA 16個引腳都輸出高電位 */
*(unsigned int*)(0x40020014) = 0xffff; // 單存操作暫存器地址
#define GPIOA_ODR *(unsigned*)0x40020014
GPIOA_ODR = 0xffff; // 使用暫存器映射
其中使用(unsigned int*)
強制轉型的作用是為了讓編譯器知道它是一個地址類型常數。通常我們為暫存器命名會考慮到它的具體意義,比如GPIOA_ODR代表該GPIO A引腳的通用輸出暫存器(Output Data Register),命名盡量便於理解為主
假設我們想實現GPIOA的暫存器控制,首先必須知道GPIOA的起始地址,於是參考STM官方網站的reference manual手冊中的記憶體映射表可以找到GPIOA的地址範圍,下圖藍色方框顯示GPIOA的起始地址為0X4002 0000
右側顯示GPIOA位於AHB1高速總線區塊上(系統的GPIO引腳都位於此處),透過data sheet的查找發現AHB1被分配到名為Block2的分區內(很明顯地片上外設都位於Block2),因此我們可以輕易地將GPIOA身處的地址標示出來,這麼做的目的是為了編寫程式時可以進行3個層次的地址偏移
這三種偏移分別是:
透過查找Memory Table表可以查到外設起始地址、GPIO起始地址以及GPIOA的地址,我們使用嵌套的方式映射這些暫存器地址
/*外設基地址*/
#define PERIPH_BASE ((unsigned int)0x40000000) // 外設起始地址
/*總線基地址*/
#define AHB1_PERIPH_BASE (PERIPH_BASE + 0x00020000) // GPIO的起始地址
/*GPIO基地址*/
#define GPIO_A_BASE (AHB1_PERIPH_BASE + 0x0000) // GPIOA起始地址
定義偏移地址的好處就是更好的擴充性,比如我今天想要define一個GPIOI地址,只需要將AHB1_PERIPH_BASE+0X2000
就好了,不需要從基地址的暫存器地址開始計算偏移
這三種層次由大到小,使用者只須要依照想要使用的引腳範圍進行定義,一旦基地址定義完成,開發者只需要選擇距離目標引腳地址最小的偏移量基地址開始定義即可,另一方面這種方式也利於開發者閱讀
GPIO端口設有10個暫存器,而且連續儲存於GPIOA的連續記憶體空間中。因此我們可以透過自定義一個結構體數據類型來模擬內存空間中的暫存器
typedef unsigned int uint32_t;
typedef struct{
uint32_t MODER;
uint32_t OTYPER;
uint32_t OSPEEDER;
uint32_t PUPDR;
uint32_t IDR;
uint32_t ODR;
uint32_t BSRR;
uint32_t LCKR;
uint32_t AFRL;
uint32_t AFGH;
}GPIO_TYPEDEF
typedef GPIO_TYPEDEF* GPIO_Typedef; // 指向GPIO結構體的指標
我們透過將指標指向GPIO_X
的基地址,使結構體內的成員的地址剛好與各個暫存器對應上,所以當我們對結構體成員操作時,事實上是在操作GPIO對應的暫存器:
GPIO_Typedef GPIO_X;
GPIO_X = GPIOX_BASE;
GPIO_X->MODER = 0X0003;
GPIO_X->OTYPER = 0X0001;
GPIO_X->OSPEEDER = 0X0003;
uint32_t tmp;
tmp = GPIO_X->IDR; // 讀取暫存器
查找完手冊上對應的外設地址,然後利用程式編寫暫存器映射的陣列指標後,我們必須再次查看手冊,釐清引腳背後每個暫存器的控制意義,GPIO暫存器的介紹在data sheet的General-purpose I/Os(GPIO)/GPIO registers下可以找到:
STM32F429的每一個GPIO端口均配置10個長度為32bits的暫存器,依種類不同大致可以分成以下5大類:
模式配置類型是GPIO引腳重要的功能部分之一,它決定引腳後續的工作性質、輸出速度以及工作狀態
進入模式配置類型暫存器介紹之前,我們先用一個蓋覽圖來抓住模式配置類型暫存器的大框架: 依照不同模式去配置不同的引腳特性
配置GPIO引腳的工作模式,包含輸入、輸出、複用功能開啟以及類比功能
GPIO端口首地址偏移量: 0x00
每個引腳由兩個位元進行控制,分別有4種不同的模式:
當GPIO選擇為輸出模式,就需要選擇輸出模式,主要有推挽模式與開漏模式兩種
設置GPIO引腳的輸出速度
為了避免引腳在沒有任何輸入或輸出(看引腳是配置成甚麼模式)下產生浮動,也就是說引腳的值是不確定的,需要依照MCU的特性去配置預設狀態的電位,PUPDR就是在處理這個問題。例如將輸入模式切換成輸出模式之間的空檔有可能會出現浮動,這時就需要配置一個確定的值
在輸出模式下使用上拉模式時,會因為ODR暫存器的預設輸出為0而影響,這時候上拉只能小幅度提升電位,輸出依然為低電位
輸出控制暫存器,當設值成0時輸出低電位;設值成1時輸出高電位。主要由比特位0~15控制16個引腳,16~31為保留位
使用ODR作為輸出控制時,其反應速度會被中斷等事件影響,造成時延。另外ODR暫存器是可讀可寫的暫存器,使用程式控制時要先對其進行讀操作,然後再進行寫操作
uint32_t tmp;
tmp = GPIO_X->ODR;
tmp = tmp | 0x0001;
GPIO_X->ODR = tmp;
置位復位暫存器,可分為高16位和低16位。低16位(0~15)控制置位操作,也就是輸出高電位;高16位(16~31)控制復位操作,也就是輸出低電位。其控制規則如下:
低16位
高16位
若動應的置位操作與復位操作同時設成1,則會以置位操作為優先。例如對引腳3的置位與復位同時賦值為1(比特位3和19),則輸出高電位
BSRR為只寫暫存器,在使用上相較於ODR,不需要讀取暫存器內容再寫入,可以直接對目標引腳進行操作,例如剛剛的引腳3例子
GPIO_X->ODR |= (1<<3); // 低16位,置位操作
GPIO_X->ODR |= (1<<16<<3); // 高16位,復位操作
輸入數據暫存器,其功能是讀取GPIO端口的所有引腳輸入狀態。低16位是只讀功能的比特位,高16位保留。
想讀取特定引腳輸入數值只要將IDR的讀值進行位元運算即可
賦用功能暫存器,可以將GPIO引腳轉為其他通訊接口功能,例如UART、SPI、I2C等等
由兩個暫存器負責處理複用功能操作,AFRL負責引腳0~7,AFRH負責8~15,每個引腳皆有16種可能,由4個比特位控制。預設狀態為AF0,並且每個引腳同時只能存在一個複用功能
其配置如下圖所示:
最後我們試著使用一個點燈程式來整合學到的暫存器觀念。引腳輸出方面,我使用GPIO A的引腳4、5、6作為紅、綠、藍LED輸出腳位
首先若要啟用GPIO,一定要先對其外設時鐘控制暫存器RCC進行置位,我們根據data sheet對Memory Map的描述找到GPIO對應的RCC地址
緊接著同樣在header文件中建立暫存器基地址映射、RCC地址映射以及GPIO暫存器結構體
#ifndef __STM32F4XX_H
#define __STM32F4XX_H
#include <stdio.h>
#include <stdint.h>
//#define GPIO_register 1
/*Memory mapping*/
#define PERIPH_BASE ((unsigned int)0x40000000)
#define AHB1_PERIPH_BASE (PERIPH_BASE + 0x00020000)
#define GPIO_H_BASE (AHB1_PERIPH_BASE + 0x1C00)
/*RCC*/
#define RCC_BASE (AHB1_PERIPH_BASE + 0x3800)
#define RCC_AHB1_ENR *(unsigned int*)(RCC_BASE+0x30)
/*GPIO*/
typedef struct{
uint32_t MODER;
uint32_t OTYPER;
uint32_t OSPEEDER;
uint32_t PUPDR;
uint32_t IDR;
uint32_t ODR;
uint16_t BSRRL;
uint16_t BSRRH;
uint32_t LCKR;
uint32_t AFRL;
uint32_t AFGH;
}GPIO_TYPEDEF;
typedef GPIO_TYPEDEF* GPIO_Typedef; //GPIO pointer
#endif
我使用兩種方式點燈,第一種是純粹的暫存器控制。第二種是封裝成類庫函式形式。主要使用GPIO_register
來切換(看mian的條件編譯),所以也附上封裝的header與source files
#ifndef __STM_GPIO__H
#define __STM_GPIO__H
#include "stm32f4xx.h"
#include <stdbool.h>
typedef enum{
port_A=0,
port_B,
port_C,
port_D,
port_E,
port_F,
port_G,
port_H
}port;
extern void GPIO_Init(GPIO_Typedef, uint8_t);
extern void GPIO_LED_Control(GPIO_Typedef, uint8_t, bool);
#endif
#include "stm_gpio.h"
void GPIO_Init(GPIO_Typedef gpio, uint8_t port){
RCC_AHB1_ENR |= (1<<port);
gpio->MODER = 0x00;
gpio->OTYPER = 0x00;
gpio->OSPEEDER = 0x00;
gpio->PUPDR = 0x00;
}
void GPIO_Config(GPIO_Typedef gpio, uint8_t pin){
gpio->MODER |= (1<<2*pin);
gpio->OTYPER |= (0<<pin);
gpio->OSPEEDER |= (2<<2*pin);
gpio->PUPDR |= (1<<2*pin);
}
void GPIO_SET(GPIO_Typedef gpio, uint8_t pin){
gpio->BSRRL &= ~(1<<pin);
gpio->BSRRL |= (1<<pin);
}
void GPIO_RESET(GPIO_Typedef gpio, uint8_t pin){
gpio->BSRRH &= ~(1<<pin);
gpio->BSRRH |= (1<<pin);
}
void GPIO_LED_Control(GPIO_Typedef gpio, uint8_t pin, bool output){
GPIO_Config(gpio, pin);
if(output)
GPIO_SET(gpio, pin);
else
GPIO_RESET(gpio, pin);
}
最後我們編寫主函式main,LED依照需求亮滅。編譯成功可以將code燒進板子檢查看看是否點燈成功
#include "stm32f4xx.h"
#include "stm_gpio.h"
/**
* main
*/
int main(void)
{
GPIO_Typedef GPIO = (GPIO_Typedef)GPIO_A_BASE;
#ifdef GPIO_register
RCC_AHB1_ENR |= (1<<0);
/*MODER*/
GPIO->MODER &= ~(3<<2*4);
GPIO->MODER &= ~(3<<2*5);
GPIO->MODER &= ~(3<<2*6);
GPIO->MODER |= (1<<2*4);
GPIO->MODER |= (1<<2*5);
GPIO->MODER |= (1<<2*6);
/*OTYPER*/
GPIO->OTYPER &= ~(1<<4);
GPIO->OTYPER &= ~(1<<5);
GPIO->OTYPER &= ~(1<<6);
GPIO->OTYPER |= (0<<4);
GPIO->OTYPER |= (0<<5);
GPIO->OTYPER |= (0<<6);
/*OSPEEDER*/
GPIO->OSPEEDER &= ~(3<<2*4);
GPIO->OSPEEDER &= ~(3<<2*5);
GPIO->OSPEEDER &= ~(3<<2*6);
GPIO->OSPEEDER |= (2<<2*4);
GPIO->OSPEEDER |= (2<<2*5);
GPIO->OSPEEDER |= (2<<2*6);
/*PUPDR*/
GPIO->PUPDR &= ~(3<<2*4);
GPIO->PUPDR &= ~(3<<2*5);
GPIO->PUPDR &= ~(3<<2*6);
GPIO->PUPDR |= (1<<2*4);
GPIO->PUPDR |= (1<<2*5);
GPIO->PUPDR |= (1<<2*6);
/*BSRRL*/
GPIO->BSRRL &= ~(1<<4);
GPIO->BSRRL &= ~(1<<5);
GPIO->BSRRL &= ~(1<<6);
// GPIO_H->BSRRL |= (1<<4);
GPIO->BSRRL |= (1<<5);
GPIO->BSRRL |= (1<<6);
/*BSRRH*/
GPIO->BSRRH &= ~(1<<4);
GPIO->BSRRH &= ~(1<<5);
GPIO->BSRRH &= ~(1<<6);
GPIO->BSRRH |= (1<<4);
// GPIO->BSRRH |= (1<<5);
// GPIO->BSRRH |= (1<<6);
#else
/* GPIO A Initial */
GPIO_Init(GPIO, port_A);
/* pin 4 config*/
GPIO_LED_Control(GPIO, 4, 0);
/* pin 5 config*/
GPIO_LED_Control(GPIO, 5, 1);
/* pin 6 config*/
GPIO_LED_Control(GPIO, 6, 0);
#endif
while(1);
}
// void SystemInit(void)
// {
// }