天天看點

《STM32MP1 M4裸機CubeIDE開發指南》第五章 STM32基礎知識入門

第五章 STM32基礎知識入門​

本章,我們着重介紹STM32的一些基礎知識,讓大家對STM32開發有一個初步的了解,為後面STM32的學習做鋪墊,友善後面的學習。本章内容大家第一次看的時候可以隻了解一個大概,後面需要用到這方面的知識的時候再回過頭來仔細看看。​

本章将分為如下幾個小節:​

5.1、C語言基礎複習;​

5.2、STM32MP157存儲系統;​

5.1 C語言基礎複習​

本小節我們講解C語言的基礎知識。C 語言知識博大精深,也不是我們三言兩語能講解清楚的,我們這裡就列舉部分STM32學習中會遇見的C 語言基礎知識點,引導那些 C 語言基礎知識不是很紮實的使用者能夠快速開發 STM32 程式,同時希望這些使用者能夠多去複習一下 C 語言基礎知識,畢竟C 語言是單片機開發中的必備基礎知識。對于 C 語言基礎比較紮實的使用者,這部分知識可以忽略不看。​

5.1.1 位操作​

C語言位操作相信學過C語言的人都不陌生了,簡而言之,就是對基本類型變量可以在位級别進行操作。這節的内容很多朋友都應該很熟練了,我這裡也就點到為止,不深入探讨。下面我們先講解幾種位操作符,然後講解位操作使用技巧。C語言支援如下6中位操作:​

運算符​ 含義​ 運算符​ 含義​
&​ 按位與​ ~​ 按位取反​
|​ 按位或​ <<​ 左移​
^​ 按位異或​ >>​ 右移​

表5.1.1.1 六種位操作​

這些與或非,取反,異或,右移,左移這些到底怎麼回事,這裡我們就不多做詳細,相信大家學C語言的時候都學習過了。如果不懂的話,可以百度一下,非常多的知識講解這些操作符。下面我們想着重講解位操作在單片機開發中的一些實用技巧。​

1,在不改變其他位的值的狀況下,對某幾個位進行設值。​

這個場景在單片機開發中經常使用,方法就是先對需要設定的位用&操作符進行清零操作,然後用|操作符設值。比如我要改變GPIOA的狀态,可以先對寄存器的值進行&清零操作:​

GPIOA->CRL &= 0XFFFFFF0F; /* 将第4~7位清0 */​      

然後再與需要設定的值進行|或運算:​

GPIOA->CRL |= 0X00000040; /* 設定相應位的值(4),不改變其他位的值 */​      

2,移位操作提高代碼的可讀性。​

移位操作在單片機開發中非常重要,下面是delay_init函數的一行代碼:​

SysTick->CTRL |= 1 << 1;​      

這個操作就是将CTRL寄存器的第1位(從0開始算起)設定為1,為什麼要通過左移而不是直接設定一個固定的值呢?其實這是為了提高代碼的可讀性以及可重用性。這行代碼可以很直覺明了的知道,是将第1位設定為1。如果寫成:​

SysTick->CTRL |= 0X0002;​      

這個雖然也能實作同樣的效果,但是可讀性稍差,而且修改也比較麻煩。​

3,~按位取反操作使用技巧​

按位取反在設定寄存器的時候經常被使用,常用于清除某一個/某幾個位。下面是delay_us函數的一行代碼:​

SysTick->CTRL &= ~(1 << 0) ; /* 關閉SYSTICK */​      

該代碼可以解讀為僅設定CTRL寄存器的第0位(最低位)為0,其他位的值保持不變。同樣我們也不使用按位取反,将代碼寫成:​

SysTick->CTRL &= 0XFFFFFFFE; /* 關閉SYSTICK */​      

可見前者的可讀性,及可維護性都要比後者好很多。​

4,^按位異或操作使用技巧​

該功能非常适合用于控制某個位翻轉,常見的應用場景就是控制LED閃爍,如:​

GPIOB->ODR ^= 1 << 5;​      

執行一次該代碼,就會使PB5的輸出狀态翻轉一次,如果我們的LED接在PB5上,就可以看到LED閃爍了。​

5.1.2 define宏定義​

define是C語言中的預處理指令,它用于宏定義(定義的是常量),可以提高源代碼的可讀性,為程式設計提供友善。常見的格式:​

#define 辨別符 字元串      

“辨別符”為所定義的宏名。“字元串”可以是常數、表達式、格式串等。例如:​

#define HSE_VALUE 8000000U      

定義辨別符HSE_VALUE的值為8000000,數字後的U表示unsigned的意思。​

至于define宏定義的其他一些知識,比如宏定義帶參數這裡我們就不多講解。​

5.1.3 ifdef條件編譯​

單片機程式開發過程中,經常會遇到一種情況,當滿足某條件時對一組語句進行編譯,而當條件不滿足時則編譯另一組語句。條件編譯指令最常見的形式為:​

#ifdef 辨別符 ​
  程式段1 ​
 #else ​
  程式段2 ​
 #endif ​      

它的作用是:當辨別符已經被定義過(一般是用#define指令定義),則對程式段1進行編譯,否則編譯程式段2。 其中#else部分也可以沒有,即:​

#ifdef ​
程式段1 ​
 #endif​      

條件編譯在HAL庫裡面是用得很多,在stm32mp1xx_hal_conf.h這個頭檔案中經常會看到這樣的語句:​

#if !defined (HSE_VALUE) ​
 #define HSE_VALUE 24000000U ​
 #endif ​      

如果沒有定義HSE_VALUE這個宏,則定義HSE_VALUE宏,并且HSE_VALUE的值為24000000U。條件編譯也是C語言的基礎知識,這裡也就點到為止吧。​

這裡提一下,24000000U中的U表示無符号整型,常見的,UL表示無符号長整型,F表示浮點型。這裡加了U以後,系統編譯時就不進行類型檢查,直接以U的形式把值賦給某個對應的記憶體,如果超出定義變量的範圍,則截取。​

5.1.4 extern變量申明​

C語言中extern可以置于變量或者函數前,以表示變量或者函數的定義在别的檔案中,提示編譯器遇到此變量和函數時在其他子產品中尋找其定義。這裡面要注意,對于extern申明變量可以多次,但定義隻有一次。在我們的代碼中你會看到看到這樣的語句:​

extern uint16_t g_usart_rx_sta; ​      

這個語句是申明g_usart_rx_sta變量在其他檔案中已經定義了,在這裡要使用到。是以,你肯定可以找到在某個地方有變量定義的語句:​

uint16_t g_usart_rx_sta; ​      

extern的使用比較簡單,但是也會經常用到,需要掌握。​

5.1.5 typedef類型别名​

typedef用于為現有類型建立一個新的名字,或稱為類型别名,用來簡化變量的定義。typedef在HAL庫用得最多的就是定義結構體的類型别名和枚舉類型了。 ​

struct _GPIO​
 {​
 __IO uint32_t CRL;​
 __IO uint32_t CRH;​
 …​
 };​
定義了      

一個結構體GPIO,這樣我們定義結構體變量的方式為:

struct _GPIO gpiox; /* 定義結構體變量gpiox */​      

但是這樣很繁瑣,HAL庫中有很多這樣的結構體變量需要定義。這裡我們可以為結體定義一個别名GPIO_TypeDef,這樣我們就可以在其他地方通過别名GPIO_TypeDef來定義結構體變量了,方法如下:​

typedef struct​
 {​
 __IO uint32_t CRL;​
 __IO uint32_t CRH;​
 …​
 } GPIO_TypeDef;​      

Typedef為結構體定義一個别名GPIO_TypeDef,這樣我們可以通過GPIO_TypeDef來定義結構體變量:​

GPIO_TypeDef gpiox;​      

這裡的GPIO_TypeDef就跟struct _GPIO是等同的作用了,但是GPIO_TypeDef使用起來友善很多。 ​

5.1.6 結構體​

1. 結構體的聲明和定義​

經常很多使用者提到,他們對結構體使用不是很熟悉,但是HAL庫中太多地方使用結構體以及結構體指針,這讓他們一下子摸不着頭腦,學習STM32的積極性大大降低,其實結構體并不是那麼複雜,這裡我們稍微提一下結構體的一些知識。​

聲明結構體類型:​

struct結構體名​
 {​
成員清單;​
 }變量名清單;​
例如:​
 struct U_TYPE ​
 {​
 int BaudRate ​
 int WordLength; ​
 }usart1, usart2;​      

在結構體申明的時候可以定義變量,也可以申明之後定義,方法是:​

struct結構體名字 結構體變量清單;​

例如:​

struct U_TYPE usart1,usart2;​      

2. 引用結構體成員變量​

結構體成員變量的引用方法是:​

結構體變量名字.成員名​

比如要引用usart1的成員BaudRate,方法是:usart1.BaudRate; 結構體指針變量定義也是一樣的,跟其他變量沒有啥差別。​

例如:​

struct U_TYPE *usart3; /* 定義結構體指針變量usart3 */​      

結構體指針成員變量引用方法是通過“->”符号實作,比如要通路usart3結構體指針指向的結構體的成員變量BaudRate,方法是:​

usart3->BaudRate;​      

3. 結構體的作用​

上面講解了結構體和結構體指針的一些知識,其他的什麼初始化這裡就不多講解了。講到這裡,有人會問,結構體到底有什麼作用呢?為什麼要使用結構體呢?下面我們将簡單的通過一個執行個體回答一下這個問題。​

在我們單片機程式開發過程中,經常會遇到要初始化一個外設比如序列槽,它的初始化狀态是由幾個屬性來決定的,比如序列槽号,波特率,極性,以及模式。對于這種情況,在我們沒有學習結構體的時候,我們一般的方法是:​

void usart_init(uint8_t usartx, uiut32_t BaudRate, uint32_t Parity, ​
uint32_t Mode);​      

這種方式是有效的同時在一定場合是可取的。但是試想,如果有一天,我們希望往這個函數裡面再傳入一個/幾個參數,那麼勢必我們需要修改這個函數的定義,重新加入新的入口參數,随着開發不斷的增多,那麼是不是我們就要不斷的修改函數的定義呢?這是不是給我們開發帶來很多的麻煩?那又怎樣解決這種情況呢?​

我們使用結構體參數,就可以在不改變入口參數的情況下,隻需要改變結構體的成員變量,就可以達到改變入口參數的目的。​

結構體就是将多個變量組合為一個有機的整體,上面的函數,usartx,BaudRate,​

Parity, Mode等這些參數,他們對于序列槽而言,是一個有機整體,都是來設定序列槽參數的,是以我們可以将他們通過定義一個結構體來組合在一個。HAL庫中是這樣定義的:​

typedef struct​
 { ​
 uint32_t BaudRate;​
 uint32_t WordLength;​
 uint32_t StopBits;​
 uint32_t Parity;​
 uint32_t Mode;​
 uint32_t HwFlowCtl;​
 uint32_t OverSampling; ​
 } UART_InitTypeDef;​      

這樣,我們在初始化序列槽的時候入口參數就可以是USART_InitTypeDef類型的變量或者指針變量了,于是我們可以改為:​

void usart_init(UART_InitTypeDef *huart);​      

這樣,任何時候,我們隻需要修改結構體成員變量,往結構體中間加入新的成員變量,而不需要修改函數定義就可以達到修改入口參數同樣的目的了。這樣的好處是不用修改任何函數定義就可以達到增加變量的目的。​

了解了結構體在這個例子中間的作用嗎?在以後的開發過程中,如果你的變量定義過多,如果某幾個變量是用來描述某一個對象,你可以考慮将這些變量定義在結構體中,這樣也許可以提高你的代碼的可讀性。​

使用結構體組合參數,可以提高代碼的可讀性,不會覺得變量定義混亂。當然結構體的作用就遠遠不止這個了,同時,HAL庫中用結構體來定義外設也不僅僅隻是這個作用,這裡我們隻是舉一個例子,通過最常用的場景,讓大家了解結構體的一個作用而已。後面一節我們還會講解結構體的一些其他知識。​

4. 結構體成員的記憶體分布與對齊​

首先我們明白下面幾點:​

(1)聲明一個結構體類型的時候是沒有為它配置設定任何存儲空間的,隻有在定義結構體變量的時候,才會為變量配置設定存儲空間。​

(2)結構體中可以有不同的資料類型成員,成員在定義時依次存儲在記憶體連續的空間中,結構體變量的首位址就是第一個成員的位址,記憶體偏移量就是各個成員相對于第一個成員位址的差(即,把低位記憶體配置設定給最先定義的變量)。​

(3)理論上,結構體所占用的存儲空間是各個成員變量所占的存儲空間之和,但是為了提高CPU的通路效率,采用了記憶體對齊方式:​

①結構體的每一個成員起始位址必須是自身類型大小的整數倍,若不足,則不足部分用資料填充至所占記憶體的整數倍。​

②結構體大小必須是結構體占用最大位元組數成員的整數倍,這樣在處理數組時可以保證每一項都邊界對齊​

根據上面的說明,我們舉例子分析如下:​

struct test​
 {​
 char a;​
 int b;​
 float c;​
 double d;​
 }mytest;​      

這個結構體所占用的記憶體怎麼算呢?理論結果為17,實際上并不是17,而是24。為什麼會這樣呢?這個就是前面我們說的記憶體對齊。​

char型變量占1個位元組,是以它的起始位址是0。int類型占用4個位元組,它的起始位址要求是4的整數倍數,那麼記憶體位址1、2、3就需要被填充(被填充的記憶體不适于變量),b從4開始。float類型也是占用4個位元組,起始位址要求是4的倍數,是以c的起始位址就是8。double類型變量占用8個位元組,起始位址為16,12~15被填充。這裡,第一個成員a的位址首地止,第二個成員b的偏移量為4,第三個成員c的偏移量是8,以此類推,是如下圖所示:​

《STM32MP1 M4裸機CubeIDE開發指南》第五章 STM32基礎知識入門

圖5.1.6. 1結構地位址記憶體配置設定​

上面的結構體成員a、b、c、d的類型各不相同,但在HAL庫中,有很多的結構體,結構體中的成員類型基本上是一樣的,例如GPIO的結構體,都是定義為uint32_t,即32位,每個成員占用4個位元組,以第一個成員MODER為首地止0,第二個成員OTYPER相對于MODER偏移為4個位元組,依次類推。關于HAL庫,我們後面會分析。​

typedef struct​
 {​
 __IO uint32_t MODER; ​
 __IO uint32_t OTYPER; ​
 __IO uint32_t OSPEEDR; ​
 __IO uint32_t PUPDR; ​
 /******此處省略部分代碼 */ ​
 __IO uint32_t VERR; ​
 __IO uint32_t IPIDR; ​
 __IO uint32_t SIDR; ​
 } GPIO_TypeDef;​      

5.1.7 關鍵字​

在core_cm4.h頭檔案中會看到有如下定義:​

#define __I volatile​
#define __O volatile ​
#define __IO volatile ​
#define __IM volatile const ​
#define __OM volatile ​
#define __IOM volatile ​      

在HAL庫中,大家會看到部分這樣的宏定義,表示将volatile或者volatile const來代替某一個符号。​

1. volatile​

volatile表示強制編譯器減少優化,告訴編譯器必須每次去記憶體中取變量值。​

程式運作時資料是存儲在主記憶體(實體記憶體)中的,每個線程先從主記憶體拷貝變量到對應的寄存器中。對沒有加volatile的變量進行讀寫時,為了提高讀取速度,編譯器進行優化時,會先把主記憶體中的變量讀取到一個寄存器中,以後,再讀取此變量的值時,就直接從該寄存器中讀取,而不是直接從記憶體中讀取了,這樣的讀寫速度比較快。如果其它程式改變了記憶體中變量的值,上面已經儲存到寄存器中的值不會跟着改變,進而造成應用程式讀取的值和實際的變量值不一緻。加了volatile以後的變量,表示不想被編譯器優化掉,每次都要從記憶體中讀取該變量的資料,不會用寄存器裡的值,這樣確定了資料的準确性,但影響了效率。​

2. const​

const叫常量限定符,用來限定特定變量為隻讀屬性,如果修改此變量,編譯器會報錯。const修飾的變量存儲在隻讀資料段,在程式結束時釋放,而const局部變量存儲在棧中,代碼塊結束時釋放。用const定義變量時就要初始化該變量:​

const int a = 4;​      

3. static​

static 關鍵字修飾的變量稱為靜态變量,如果該變量在聲明時未賦初始值,則編譯器自動初始化為0,靜态變量存儲在全局區(靜态區)。​

在函數内被static聲明的變量,僅能在本函數中使用,也叫靜态局部變量。​

在檔案内(函數體外)被static聲明的變量,僅能被本檔案内的函數通路,不能被其他檔案中的函數通路,也叫靜态全局變量。​

靜态全局變量和普通的全局變量不同,靜态全局變量僅限于本檔案中使用,在其它檔案中可以定義一個與靜态全局變量名字相同的變量。普通的全局變量可以通過extern外部聲明後被其他檔案使用,也就是整個工程可見,而且其他檔案不能再定義一個與普通全局變量名字相同的變量了。​

用static修飾的函數和用static修飾的變量類似。​

5.1.8 指針​

指針是一個值指向位址的變量(或常量),其本質是指向一個位址,進而可以通路一片記憶體區域。在編寫STM32代碼的時候,或多或少都要用到指針,它可以使不同代碼共享同一片記憶體資料,也可以用作複雜的連結性的資料結構的建構,比如連結清單,鍊式二叉樹等,而且,有些地方必須使用指針才能實作,比如記憶體管理等。​

申明指針我們一般以p開頭,如:​

char * p_str = “This is a test!”;​      

這樣,我們就申明了一個p_str的指針,它指向This is a test!這個字元串的首位址。我們編寫如下代碼:​

int main(void)​
 {​
 HAL_Init();   /* 初始化HAL庫 */​
 sys_stm32_clock_init(RCC_PLL_MUL9);  /* 設定時鐘,72M */​
 usart_init(115200);  /* 初始化序列槽 */​

 uint8_t temp = 0X88;   /* 定義變量 temp */​
 uint8_t *p_num = &temp;    /* 定義指針p_num,指向temp的位址 */​
 printf("temp:0X%X\r\n", temp); /* 列印temp的值 */​
 printf("*p_num: 0X %X\r\n", *p_num); /* 列印*p_num的值 */​
 printf("p_num: 0X %X\r\n", (uint32_t)p_num); /* 列印p_num的值 */​
 printf("&p_num: 0X %X\r\n", (uint32_t)&p_num);/* 列印&p_num的值 */​

 while (1); ​
 }​      

此代碼的輸出結果為:​

《STM32MP1 M4裸機CubeIDE開發指南》第五章 STM32基礎知識入門

圖5.1.6. 2輸出結果​

p_num:是uint8_t類型指針,指向temp變量的位址,其值等于temp變量的位址。​

*p_num:取p_num指向的位址所存儲的值,即temp的值。​

&p_num:取p_num指針的位址,即指針自身的位址。​

以上,就是指針的簡單使用和基本概念說明,指針的詳細知識和使用範例大家可以百度學習,網上有非常多的資料可供參考。指針是C語言的精髓,在後續的代碼中我們将會大量用到各種指針,大家務必好好學習和了解指針的使用。​

5.2 STM32MP157存儲系統​

5.2.1 寄存器基本概念​

我們在接觸51單片機的時候就經常聽到寄存器(Register)這個詞,筆者認為,寄存有“寄養、臨時存放”之意。寄存器是CPU内部的元件,其實也就是由鎖存器或觸發器構成的,是​​CPU​​内部用來暫時存放參與運算的資料的一些小型存儲區域,在數字電路中這些資料就是0或者1的二進制資料或代碼。​

寄存器擁有非常高的讀寫速度。當CPU在計算時,先把要用的資料從存儲子產品讀到記憶體,然後再把即将參與運算的資料寫到寄存器中,當要用這些資料進行運算的時候,再從這些寄存器中讀取出資料,運算的結果也可以從記憶體中寫到寄存器,這樣一來,CPU就不用每次從記憶體中讀取資料了,這使CPU的計算能力加快了。​

STM32單片機的寄存器是32位的,而且寄存器數量比51單片機的寄存器數量還要多。CPU中有核心外設和片上外設,每種外設都有其對應的寄存器,通過配置這些寄存器可以控制對應的外設實作複雜的功能。核心外設(核外設),如我們後面會遇見的SysTick(系統滴答定時器),片上外設,如我們常見的GPIO、SPI、IIC、UART、I2C等。​

STM32的寄存器那麼多,我們不可能把每個寄存器的配置都記住,在開發過程中,我們用到什麼外設就查詢對應外設的寄存器。從大方向來區分,STM32寄存器分為兩類,如下表所示:​

大類​ 小類​ 說明​
核心寄存器​ 核心相關寄存器​ 包含R0~R15、xPSR、特殊功能寄存器等​
中斷控制寄存器​ 包含NVIC和SCB相關寄存器,NVIC有:ISER、ICER、ISPR、IP等;SCB有:VTOR、AIRCR、SCR等​
SysTick寄存器​ 包含CTRL、LOAD、VAL和CALIB四個寄存器​
記憶體保護寄存器​ 可選功能,取決于廠商晶片設計​
調試系統寄存器​ ETM、ITM、DWT、IPIU等相關寄存器​
外設寄存器​ 包含GPIO、UART、IIC、SPI、TIM、DMA、ADC、DAC、RTC、I/WWDG、PWR、CAN、USB等各種外設寄存器​

表5.2.1. 1寄存器分類​

其中,核心寄存器,我們一般隻需要關心中斷控制寄存器和SysTick寄存器即可,如果要深入研究RTOS作業系統的話就需要了解R0~R15、xPSR這些核心寄存器。外設寄存器則是學到哪個外設,就了解哪個外設相關寄存器即可,是以整體來說,我們需要關心的寄存器并不是很多,而且很多都是有共性的,比如STM32MP157 M4核心有6個IIC控制器,我們隻需要學習其中一個IIC控制器相關寄存器即可,其他5個都是一樣的。​

5.2.2總線結構​

本小節我們對總線結構做一個了解性的介紹。​

AMBA (Advanced Microcontroller Bus Architecture) 進階處理器總線架構,是由ARM公司推出的一種通用的、開放的片上通信标準,用于片上系統中功能子產品的連接配接和管理。其中,AHB、APB和AXI總線是目前的SOC中經常用到的總線結構:​

  • AXI(Advanced eXtensible Interface),即進階的可擴充接口,是一種面向高性能和高時鐘頻率的系統設計的總線,目前AMBA4.0 包括AXI4.0、AXI4.0-lite、ACE4.0、AXI4.0-stream等。​
  • AHB(Advanced High performance Bus),即進階高性能總線,主要用于高性能子產品 (如CPU、DMA和DSP等)之間的連接配接,也可以稱AHB為“系統總線”。​
  • APB(AdvancedPeripheral Bus),即進階外圍總線,主要用于低帶寬的周邊外設(如UART、USB、SPI、I2C等)之間的連接配接,也稱之為外圍總線。​

如下圖是STM32MP157的總線架構圖,總線架構圍繞兩個互連矩陣進行組織。其中,AXI互聯矩陣主要用于Cortex-A7 CPU子系統和高帶寬的裝置(USBH、ETH、SDMMC1/2、MDMA、GPU和 LTDC)。主AHB互聯矩陣主要用于Cortex-M4子系統和相關的裝置(OTG、DMA1/2和SDMMC3)。​

《STM32MP1 M4裸機CubeIDE開發指南》第五章 STM32基礎知識入門

圖5.2.2. 1總線架構圖​

如上圖中,M表示主端口,S表示從端口。兩根線交叉線相連有小圓點的地方表示可以進行通信,沒有小圓點的地方表示不能通信。在AXI互聯矩陣中,M0~M10共接了11個主機,S0~S11共接了12個從機層。其中,M0端口連接配接到了MCU互連端口,MCU可以通過此端口通路MPU的子產品,不過M0和S7線沒有交叉圓點,表示MCU無法通路MPU的ROM。M1和M2接了USBH端口,它們僅限于DDR和SYSRAM存儲器的通路。​

主AHB互聯矩陣是有關于Cortex-M4子系統的AHB互聯部分,其結構繼承了以前的MCU系列,包括10個主機和9個從機層,并由 32 位多層 AHB 總線矩陣構成。​

對于STM32MP157的總線架構圖,這裡就不做重點講解,我們主要能大概看明白此圖的含義就可以了,如果對AMBA總線架構感興趣,也可以在ARM官網擷取更加詳細的資訊。​

5.2.3 存儲器映射​

STM32MP157是一個32位ARM晶片,它可以很友善的通路4GB以内的存儲空間(2^32 = 4GB),這4GB的存儲空間不是雜亂無章的,而是在晶片設計的時候規劃好了不同外設所占用的存儲範圍,比如内部SRAM、DDR、外設等,這些外設子產品全部組織在同一個4GB的線性位址空間内。資料位元組以小端格式(小端模式)存放在存儲器中,資料的高位元組儲存在記憶體的高位址中,而資料的低位元組儲存在記憶體的低位址中。STM32MP157的記憶體映射如下圖所示:​

《STM32MP1 M4裸機CubeIDE開發指南》第五章 STM32基礎知識入門

圖5.2.3. 1記憶體映射​

從上圖中左側部分可以看到,STM32MP157整個4GB記憶體空間被分為13塊,這13塊存儲區域對應不同的功能。因為晶片廠商是不可能把4GB空間用完的,同時,為了友善後續型号更新,會将一些空間預留(Reserved),圖中灰色框就是未使用的預留存儲塊,13個存儲塊的功能如下表所示:​

存儲塊​ 功能​ 位址範圍​
BOOT​ BOOT ROM區域​ 0X00000000~0X0FFFFFFF(256MB)​
SRAMs​ SRAM區域​ 0X10000000~0X1FFFFFFF(256MB)​
SYSRAM​ SYSRAM區域​ 0X20000000~0X2FFFFFFF(256MB)​
RAM aliases①​ 未知,ST未給出詳細介紹​ 0X30000000~0X3FFFFFFF(256MB)​
Peripherals 1​ 外設記憶體區域1​ 0X40000000~-X4FFFFFFF(256MB)​
Peripherals 2​ 外設記憶體區域2​ 0X50000000~0X5FFFFFFF(256MB)​
FMC NOR​ FMC接口NOR Flash映射後的記憶體區域​ 0X60000000~0X6FFFFFFF(256MB)​
QUADSPI​ QUAD SPI Flash映射後的記憶體區域​ 0X70000000~0X7FFFFFFF(256MB)​
FMC NAND​ FMC接口NAND Flash映射後的記憶體區域​ 0X80000000~0X8FFFFFFF(256MB)​
STM​ STM寄存器區域​ 0X90000000~0X9FFFFFFF(256MB)​
CA7​ Cortex-A7核心區域​ 0XA0000000~0XBFFFFFF(512MB)​
DDR​ DDR記憶體區域​ 0XC0000000~0XDFFFFFF(512MB)​
DDR擴充(僅僅CA7)或調試​ DDR擴充或調試區域​ 0XE0000000~0XFFFFFFF(512MB)​

表5.2.3. 1存儲塊功能及位址範圍​

根據STM32MP157官方文檔描述,STM32MP157有SRAM1~4,但是現在SRAMs和RAM aliases都有SRAM1~4,實際測試M4代碼放到SRAMs和RAM aliases區域都可以運作。但是ST官方M4例程都是放到SRAMs中的SRAM1~4中運作,不清楚RAM aliases中的SRAM1~4有什麼作用,ST官方的資料也沒有明确說明。​

我們來看幾個比較重要的存儲塊,首先是BOOT存儲區域,這是STM32MP157内部BOOT ROM,用來存儲ST自己編寫的啟動代碼,使用者不可以使用。​

第二個就是SRAMs區域,這個區域裡面一共有4塊SRAM:SRAM1~SRAM4,這四塊SRAM位址範圍如下表所示:​

存儲塊​ 記憶體區域​ 大小​
SRAM1​ 0X10000000~0X1001FFFF​ 128KB​
SRAM2​ 0X10020000~0X1003FFFF​ 128KB​
SRAM3​ 0X10040000~0X1004FFFF​ 64KB​
SRAM4​ 0X10050000~0X1005FFFF​ 64KB​

表5.2.3. 2存儲區域​

SRAM1~SRAM4的位址空間是連續的,位址範圍為0X10000000~0X1005FFFF,總大小為384KB。STM32MP157 M4核心就使用這384KB記憶體來運作程式,RAM和ROM全部都在這384KB記憶體範圍内。​

但是,如果既要啟動A7核心,又要啟動M4核心,并且A7核心和M4核心之間又要進行通信,那麼M4就不能使用這全部的384KB記憶體。此時ST給出的SRAM1~SRAM4記憶體配置設定如下圖所示:​

《STM32MP1 M4裸機CubeIDE開發指南》第五章 STM32基礎知識入門

圖5.2.3. 2 記憶體配置設定​

從上圖可以看出,當A7和M4一起啟動,而且A7要和M4之間進行通信的時候,SRAM4作為DMA緩沖區,SRAM3作為内部IPC緩沖區。此時,M4核心隻能使用SRAM1來存放代碼,使用SRAM2存放資料,這個時候M4的代碼和資料都不能大于128KB!大家打開ST官方的M4裸機例程的時候就會發現預設的代碼區為SRAM1,資料區為SRAM2。​

圖中的RETRAM僅能用于存放M4核心的中斷向量表!關于中斷向量表我們在後面外部中斷實驗章節會進行介紹。​

第三個就是Peripherals 1區域,也就是外設記憶體區域1,外設都是挂在對應的總線上,這些總線都有對應的記憶體空間,Perpherals 1區域記憶體配置設定如下表所示:​

總線​ 記憶體區域​ 描述​
APB1​ 0X40000000~0X4001C3FF​ I2C/DAC/TIM/UART/USART等相關外設​
APB2​ 0X44000000~0X440137FF​ CAN/SAI/TIM/USART等相關外設​
AHB2​ 0X48000000~0X4903FFFF​ DAM/ADC/SDMMC2等相關外設​
AHB3​ 0X4C000000~0X4C0063FF​ HSEM/IPCC/硬體加速等相關外設​

表5.2.3. 3記憶體區域​

第四個是Peripherals 2區域,是外設記憶體區域2,對應的記憶體配置設定如表5.2.3.4所示:​

總線​ 記憶體區域​ 描述​
AHB4​ 0X50000000~0X5001FFFF​ PWR/RCC/GPIOs等相關外設​
APB3​ 0X50020000~0X5002A3FF​ SYSCFG/LPTIM/SAI/HDP等相關外設​
APB-DBG​ 0X50080000~0X500DDFFF​ Coresight IP​
AHB5​ 0X54000000~0X540043FF​ Backup RAM/硬體加速器等相關外設​
AXIMC​ 0X57000000~0X570FFFFF​ AXIMC​
AHB6​ 0X58000000~0X5903FFFF​ USBH/ETH/SDMMC/MDMA/GPU等相關外設​
APB4​ 0X5A000000~0X5A0073FF​ DDRC/DDRPHY/DDRPERFM/LTDC/DSIHOST/USBPHYC等相關外設​
APB5​ 0X5C000000~0X5C00A3FF​ 安全相關IP/TZC/TZPC等相關外設​

表5.2.3. 4記憶體區域​

第三個就是DDR區域,這個區域就是DDR映射的記憶體區域,這個區域位址範圍為0XC0000000~0XFFFFFFFF,一共1GB,是以ST32MP157最大支援1GB的DDR記憶體。​

STM32MP157存儲映射就講到這裡,至于其他存儲區域,大家了解一下就行了,對于STM32MP157 M4核心來說,重點要了解的就是SRAM1~4這384KB記憶體,因為M4核心的程式就是運作在這段記憶體區域内的。​

5.2.4 寄存器位址​

前面我們了解了總線、存儲器映射,可以知道總線的位址以及總線上有哪些外設,我們打開開發闆CD光牒A-基礎資料\7、STM32MP1參考資料中的《STM32MP157參考手冊》,在整個學習過程中我們需要不斷翻看此手冊。檢視參考手冊的寄存器邊界位址表格Register boundary addresses,此表中詳細記錄了每個外設挂在哪個總線上,例如,GPIOZ挂在AHB5上,GPIOA~GPIOK挂在AHB4上,如下圖隻是部分截圖:​

《STM32MP1 M4裸機CubeIDE開發指南》第五章 STM32基礎知識入門

圖5.2.4. 1寄存器邊界位址表格​

根據Register boundary addresses表,我們以AHB4這根總線為例,0x50000000表示AHB4總線的基位址,AHB4總線位址範圍為0x50000000 ~0x5001FFFF,這一段位址區間被劃分為若幹的小區域,這些小區域用于保留或者給不同的外設使用。例如0x5000D400~0x5001FFFF這段區域用于保留,0x5000A000~0x5000A3FF這段區域給GPIOI使用,其中0x5000A000表示GPIOI的基位址,我們點選GPIOI後面的GPIO registers一欄就進入GPIO相關的寄存器配置介紹頁面。​

如下圖,其中,寄存器GPIOx_MODER(x等于A~K和Z,下同)的偏移位址是0x00,複位後預設的實際位址是0xFFFF FFFF,如果我們繼續往下看,下一個寄存器GPIOx_OTYPER的偏移位址是0x04,他們是以0x04為間隔逐漸遞增的,即相鄰的兩個寄存器的偏移位址是0x04。STM32位址的編排是按照Byte來編排的,也就是8位,那麼32位的資料就占用了4個Byte才構成了一個完整的32位寄存器,是以位址的偏移是相隔4(0x04)Byte。​

《STM32MP1 M4裸機CubeIDE開發指南》第五章 STM32基礎知識入門

圖5.2.4. 2寄存器配置頁面​

上圖中,寄存器GPIOx_MODER有0~31個位,每位都可以進行讀(r)和寫(w)操作,其中每兩位組合成一個MODERy(y等于0~15),對某位寫1表示将該位置位,寫0表示将該位複位。根據寄存器位說明,對某一組MODERy寫入00表示配置某個IO口(引腳)為輸入模式,寫入01表示配置某個IO口(引腳)為輸出模式。例如控制GPIOI的第0個IO口為輸出模式,則配置GPIOI的MODER0[1:0]為01(即第0位為1,第1位為0)。​

我們找到參考手冊第13章GPIO介紹部分,章節13.1~13.3主要是GPIO的整體性能介紹,13.4部分是GPIO的各個寄存的每個位的配置介紹,如果要配置GPIO寄存器,就按照這章節的内容來操作,對于其它外設寄存器也是如此。​

《STM32MP1 M4裸機CubeIDE開發指南》第五章 STM32基礎知識入門

圖5.2.4. 3介紹​

上面提到偏移位址,偏移位址是相對基址偏移後的另外一個位址量。實際位址(或者說絕對位址)就是基位址加上偏移位址後的值。根據表Register boundary addresses我們已經可以找到基位址,再根據查詢到的偏移位址就可以算出某個寄存器的實際位址(基位址+偏移位址)。例如GPIOI這個外設的位址範圍是0x5000A000~0x5000A3FF,0x5000A000是GPIOI的基位址,可以算出GPIOI_MODER寄存器的位址為0x5000A000(0x5000A000+0x00),GPIOI_ODR寄存器的位址為0x5000A014(0x5000A000+0x14)。​

5.2.5 寄存器映射​

通過前面存儲器映射以及寄存器的介紹,寄存器映射的概念就更好了解了。給有特定功能的記憶體單元取一個别名,這個别名就是我們經常說的寄存器,給已經配置設定好位址的有特定功能的記憶體單元取别名的過程就叫寄存器映射。​

例如,我們在51單片機中為什麼可以用 P0=0xFF 點亮小燈,這是因為在其頭檔案reg52.h中利用sfr這個關鍵字定義了一個個的記憶體位址,sfr(special function register)即特殊功能寄存器,sfr P0=0x80表示把單片機的位址0x80改名字為P0,這就意味這我們可以用P0代表80這個位址單元,如果要操作80這個位址,直接操作P0即可。但是在STM32中沒有sfr這個關鍵字,那它是如何實作的呢?​

GPIOB的位址為0x50003000 - 0x500033FF,在這段位址中有很多的寄存器,包括寄存器GPIOB_ODR(偏移位址位0x14),GPIOB_ODR寄存的位址為0x50003014(絕對位址),如果GPIOB_ODR的低十六位全為1即可點亮16個小燈,那麼:​

将這16位置1,可以寫成:​

*(unsigned int*)(0x50003014) = 0xFFFF;​      
#define GPIOB_ODR (unsignedint*)(0x50003014)​
 * GPIOB_ODR = 0xFF;​
或​
#define GPIOB_ODR *(unsignedint*)(0x50003014)​
 GPIOB_ODR = 0xFF;​      
GPIOB->ODR = 0XFF;​