正點原子-戰艦V3第四章 STM32F1 基礎知識入門

2020-12-07 正點原子

這一章,我們將著重 STM32 開發的一些基礎知識,讓大家對 STM32 開發有一個初步的了解,為後面 STM32 的學習做一個鋪墊,方便後面的學習。這一章的內容大家第一次看的時候可以只了解一個大概,後面需要用到這方面的知識的時候再回過頭來仔細看看。這章我們分 7個小結,

·4.1 MDK 下 C 語言基礎複習

·4.2 STM32F1 系統架構

·4.3 STM32F103 時鐘系統

·4.4 IO 引腳復用器和映射

·4.5 STM32F1 NVIC 中斷優先級管理

·4.6 MDK 中寄存器地址名稱映射分析

·4.7 MDK 固件庫快速開發技巧

4.1 MDK 下 C 語言基礎複習

這一節我們主要講解一下 C 語言基礎知識。C 語言知識博大精深,也不是我們三言兩語能講解清楚,同時我們相信學 STM32F4 這種級別 MCU 的用戶,C 語言基礎應該都是沒問題的。我們這裡主要是簡單的複習一下幾個 C 語言基礎知識點,引導那些 C 語言基礎知識不是很紮實的用戶能夠快速開發 STM32 程序。同時希望這些用戶能夠多去複習一下 C 語言基礎知識,C 語言畢竟是單片機開發中的必備基礎知識。對於 C 語言基礎比較紮實的用戶,這部分知識可以忽略不看。

4.1.1 位操作

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

表 4.1.1 16 種位操作

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

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

操作GPIOA->ODR &=0XFF0F; //將第 4-7 位清 0

然後再與需要設置的值進行|或運算

GPIOA->ODR |=0X0040; //設置相應位的值,不改變其他位的值2) 移位操作提高代碼的可讀性。

移位操作在單片機開發中也非常重要,我們來看看下面一行代碼

GPIOA->ODR| = 1 << 5;

這個操作就是將 ODR 寄存器的第 5 位設置為 1,為什麼要通過左移而不是直接設置一個固定的值呢?其實,這是為了提高代碼的可讀性以及可重用性。這行代碼可以很直觀明了的知道,是將第 5 位設置為 1,其他位的值不變。如果你寫成

GPIOA->ODR =0x0020;

這樣的代碼可讀性非常差同時也不好重用。

3) ~取反操作使用技巧

例如 GPIOA->ODR 寄存器的每一位都用來設置一個 IO 口的輸出狀態,某個時刻我們希望去設置某一位的值為 0,同時其他位都為 1,簡單的作法是直接給寄存器設置一個值:

GPIOA->ODR =0xFFF7;

這樣的作法設置第 3 位為 0,但是這樣的寫法可讀性很差。看看如果我們使用取反操作怎麼實現:

GPIOA->ODR= (uint16_t)~(1<<3);

看這行代碼應該很容易明白,我們設置的是 ODR 寄存器的第 3 位為 0,其他位為 1,可讀性非常強。

4.1.2 define 宏定義

define 是 C 語言中的預處理命令,它用於宏定義,可以提高原始碼的可讀性,為編程提供方便。常見的格式:

#define 標識符 字符串

「標識符」為所定義的宏名。「字符串」可以是常數、表達式、格式串等。例如:#define HSI_VALUE ((uint32_t)16000000)

定義標識符 HSI_VALUE 的值為 16000000。這樣我們就可以在代碼中直接使用標識符HSI_VALUE,而不用直接使用常量 16000000,同時也很方便我們修改 HSI_VALUE 的值。至於 define 宏定義的其他一些知識,比如宏定義帶參數這裡我們就不多講解。

4.1.3# ifdef 和 #if defined 條件編譯

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

#ifdef 標識符

程序段 1

#else

程序段 2

#endif

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

#ifdef

程序段 1

#endif

這個條件編譯在 MDK 裡面是用得很多的,在 stm32f4xx_hal_conf.h 這個頭文件中會看到這樣的語句:

#ifdef HAL_GPIO_MODULE_ENABLED

#include "stm32f1xx_hal_gpio.h"

#endif

這段代碼的作用是判斷宏定義標識符 HAL_GPIO_MODULE_ENABLED 是否被定義,如果被定義了,那麼就引入頭文件 stm32f1xx_hal_gpio.h。

對於條件編譯,還有個常用的格式,如下:

#if defined XXX1

程序段 1

#elif defined XXX2

程序段 2

#elif defined XXXn

程序段 n

#endif

這種寫法的作用實際跟 ifdef 很相似,不同的是 ifdef 只能在兩個選擇中判斷是否定義,而 if defined 可以在多個選擇中判斷是否定義。

條件編譯也是 c 語言的基礎知識,這裡就給大家講解到這裡,不懂的大家可以查看在網上搜索相關資料學習。

4.1.4 extern 變量申明

C 語言中 extern 可以置於變量或者函數前,以表示變量或者函數的定義在別的文件中,提示編譯器遇到此變量和函數時在其他模塊中尋找其定義。這裡面要注意,對於 extern 申明變量可以多次,但定義只有一次。在我們的代碼中你會看到看到這樣的語句:

extern u16 USART_RX_STA;

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

u16 USART_RX_STA;

的出現。下面通過一個例子說明一下使用方法。

在 Main.c 定義的全局變量 id,id 的初始化都是在 Main.c 裡面進行的。

Main.c 文件

u8 id;//定義只允許一次

main()

{

id=1;

printf("d%",id);//id=1

test();

printf("d%",id);//id=2

}

但是我們希望在main.c的 changeId(void)函數中使用變量id,這個時候我們就需要在main.c裡面去申明變量 id 是外部定義的了,因為如果不申明,變量 id 的作用域是到不了 main.c 文件中。看下面 main.c 中的代碼:

extern u8 id;//申明變量 id 是在外部定義的,申明可以在很多個文件中進行

void test(void){

id=2;

}

在 main.c 中申明變量 id 在外部定義,然後在 main.c 中就可以使用變量 id 了。

對於 extern 申明函數在外部定義的應用,這裡我們就不多講解了。

4.1.5 typedef 類型別名

typedef 用於為現有類型創建一個新的名字,或稱為類型別名,用來簡化變量的定義。typedef 在 MDK 用得最多的就是定義結構體的類型別名和枚舉類型了。

struct _GPIO

{

__IO uint32_t MODER;

__IO uint32_t OTYPER;

};

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

struct _GPIO GPIOA;//定義結構體變量 GPIOA

但是這樣很繁瑣,MDK 中有很多這樣的結構體變量需要定義。這裡我們可以為結體定義一個別

名 GPIO_TypeDef,這樣我們就可以在其他地方通過別名 GPIO_TypeDef 來定義結構體變量了。

方法如下:

typedef struct

{

__IO uint32_t MODER;

__IO uint32_t OTYPER;

} GPIO_TypeDef;

Typedef 為結構體定義一個別名 GPIO_TypeDef,這樣我們可以通過 GPIO_TypeDef 來定義結構體

變量:

GPIO_TypeDef _GPIOA,_GPIOB;

這裡的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了。 這樣是不是方便很多?

4.1.6 結構體

經常很多用戶提到,他們對結構體使用不是很熟悉,但是 MDK 中太多地方使用結構體以及

結構體指針,這讓他們一下子摸不著頭腦,學習 STM32 的積極性大大降低,其實結構體並不是

那麼複雜,這裡我們稍微提一下結構體的一些知識,還有一些知識我們會在下一節的「寄存器

地址名稱映射分析」中講到一些。

聲明結構體類型:

Struct 結構體名{

成員列表;

}變量名列表;

例如:

Struct G_TYPE {

uint32_t Pin;

uint32_t Mode;

uint32_t Speed;

}GPIOA,GPIOB;

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

Struct 結構體名字 結構體變量列表 ;

例如:struct G_TYPE GPIOA,GPIOB;

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

結構體變量名字.成員名

比如要引用 GPIOA 的成員 Mode,方法是:GPIOA. Mode;

結構體指針變量定義也是一樣的,跟其他變量沒有啥區別。

例如:struct G_TYPE *GPIOC;//定義結構體指針變量 GPIOC;

結構體指針成員變量引用方法是通過「->」符號實現,比如要訪問 GPIOC 結構體指針指向的結

構體的成員變量 Speed,方法是:

GPIOC-> Speed;

上面講解了結構體和結構體指針的一些知識,其他的什麼初始化這裡就不多講解了。講到這裡,

有人會問,結構體到底有什麼作用呢?為什麼要使用結構體呢?下面我們將簡單的通過一個實

例回答一下這個問題。

在我們單片機程序開發過程中,經常會遇到要初始化一個外設比如 IO 口。它的初始化狀態

是由幾個屬性來決定的,比如模式,速度等。對於這種情況,在我們沒有學習結構體的時候,

我們一般的方法是:

void HAL_GPIO_Init (uint32_t Pin, uint32_t Mode, uint32_t Speed);

這種方式是有效的同時在一定場合是可取的。但是試想,如果有一天,我們希望往這個函數裡

面再傳入一個參數,那麼勢必我們需要修改這個函數的定義,重新加入上下拉 Pull 這個入口參

數。於是我們的定義被修改為:

void HAL_GPIO_Init (uint32_t Pin, uint32_t Mode, uint32_t Speed,uint32_t Pull);

但是如果我們這個函數的入口參數是隨著開發不斷的增多,那麼是不是我們就要不斷的修改函

數的定義呢?這是不是給我們開發帶來很多的麻煩?那又怎樣解決這種情況呢?

這樣如果我們使用到結構體就能解決這個問題了。我們可以在不改變入口參數的情況下,

只需要改變結構體的成員變量,就可以達到上面改變入口參數的目的。

結構體就是將多個變量組合為一個有機的整體。上面的函數中 Pin, Mode,

Speed 和 Pull 這些參數,他們對於 GPIO 而言,是一個有機整體,都是來設置 IO 口參數的,所

以我們可以將他們通過定義一個結構體來組合在一個。MDK 中是這樣定義的:

typedef struct

{

uint32_t Pin;

uint32_t Mode;

uint32_t Pull;

uint32_t Speed;

}GPIO_InitTypeDef;

於是,我們在初始化 GPIO 口的時候入口參數就可以是 GPIO_InitTypeDef 類型的變量或者指針

變量了,MDK 中是這樣做的:

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);

這樣,任何時候,我們只需要修改結構體成員變量,往結構體中間加入新的成員變量,而不需

要修改函數定義就可以達到修改入口參數同樣的目的了。這樣的好處是不用修改任何函數定義

就可以達到增加變量的目的。

理解了結構體在這個例子中間的作用嗎?在以後的開發過程中,如果你的變量定義過多,

如果某幾個變量是用來描述某一個對象,你可以考慮將這些變量定義在結構體中,這樣也許可

以提高你的代碼的可讀性。

使用結構體組合參數,可以提高代碼的可讀性,不會覺得變量定義混亂。當然結構體的作

用就遠遠不止這個了,同時,MDK 中用結構體來定義外設也不僅僅只是這個作用,這裡我們只

是舉一個例子,通過最常用的場景,讓大家理解結構體的一個作用而已。後面一節我們還會講

解結構體的一些其他知識。

4.2 STM32F1 系統架構

STM32 的系統架構比 51 單片機就要強大很多了。STM32 系統架構的知識可以在《STM32

中文參考手冊 V10》的 P25~28 有講解,這裡我們也把這一部分知識抽取出來講解,是為了大

家在學習 STM32 之前對系統架構有一個初步的了解。這裡的內容基本也是從中文參考手冊中

參考過來的,讓大家能通過我們手冊也了解到,免除了到處找資料的麻煩吧。如果需要詳細深

入的了解 STM32 的系統架構,還需要在網上搜索其他資料學習學習。

我們這裡所講的 STM32 系統架構主要針對的 STM32F103 這些非互聯型晶片。首先我們看

看 STM32 的系統架構圖:

圖 4.2.1STM32 系統架構圖

STM32 主系統主要由四個驅動單元和四個被動單元構成。

四個驅動單元是:

內核 DCode 總線;

系統總線;

通用 DMA1;

通用 DMA2;

四被動單元是:

AHB 到 APB 的橋:連接所有的 APB 設備;

內部 FlASH 快閃記憶體;

內部 SRAM;

FSMC;

下面我們具體講解一下圖中幾個總線的知識:

① ICode 總線:該總線將 M3 內核指令總線和快閃記憶體指令接口相連,指令的預取在該總線上

面完成。

② DCode 總線:該總線將 M3 內核的 DCode 總線與快閃記憶體存儲器的數據接口相連接,常量

加載和調試訪問在該總線上面完成。

③ 系統總線:該總線連接 M3 內核的系統總線到總線矩陣,總線矩陣協調內核和 DMA 間

訪問。

④ DMA 總線:該總線將 DMA 的 AHB 主控接口與總線矩陣相連,總線矩陣協調 CPU 的

DCode 和 DMA 到 SRAM,快閃記憶體和外設的訪問。

⑤ 總線矩陣:總線矩陣協調內核系統總線和 DMA 主控總線之間的訪問仲裁,仲裁利用

輪換算法。

⑥ AHB/APB 橋:這兩個橋在 AHB 和 2 個 APB 總線間提供同步連接,APB1 操作速度限於

36MHz,APB2 操作速度全速。

對於系統架構的知識,在剛開始學習 STM32 的時候只需要一個大概的了解,大致知道是個

什麼情況即可。對於尋址之類的知識,這裡就不做深入的講解,中文參考手冊都有很詳細的講

解。

4.3 STM32F103 時鐘系統

STM32F1 時鐘系統的知識在《STM32 中文參考手冊 V10》第六章復位和時鐘控制章節有非

常詳細的講解,網上關於時鐘系統的講解也基本都是參考的這裡。這些知識也不是什麼原創,

純粹根據官方提供的中文參考手冊和自己的應用心得來總結的,如有不合理之處望大家諒解。

這部分內容我們分 3 個小節來講解:

·4.3.1 STM32F103 時鐘樹概述

·4.3.2 STM32F103 時鐘初始化配置

·4.3.3 STM32F103 時鐘使能和配置

4.3.1 STM32F103 時鐘樹概述

眾所周知,時鐘系統是 CPU 的脈搏,就像人的心跳一樣。所以時鐘系統的重要性就不言而

喻了。STM32F103的時鐘系統比較複雜,不像簡單的51單片機一個系統時鐘就可以解決一切。

於是有人要問,採用一個系統時鐘不是很簡單嗎?為什麼 STM32 要有多個時鐘源呢? 因為首

先 STM32 本身非常複雜,外設非常的多,但是並不是所有外設都需要系統時鐘這麼高的頻率,

比如看門狗以及 RTC 只需要幾十 k 的時鐘即可。同一個電路,時鐘越快功耗越大,同時抗電磁

幹擾能力也會越弱,所以對於較為複雜的 MCU 一般都是採取多時鐘源的方法來解決這些問題。

首先讓我們來看看 STM32F103 的時鐘系統圖:

圖 4.3.1.1 STM32F103 時鐘系統圖

在 STM32 中,有五個時鐘源,為 HSI、HSE、LSI、LSE、PLL。從時鐘頻率來分可以分為

高速時鐘源和低速時鐘源,在這 5 個中 HIS,HSE 以及 PLL 是高速時鐘,LSI 和 LSE 是低速時

鍾。從來源可分為外部時鐘源和內部時鐘源,外部時鐘源就是從外部通過接晶振的方式獲取時

鍾源,其中 HSE 和 LSE 是外部時鐘源,其他的是內部時鐘源。下面我們看看 STM32 的 5 個時

鍾源,我們講解順序是按圖中紅圈標示的順序:

①、HSI 是高速內部時鐘,RC 振蕩器,頻率為 8MHz。

②、HSE 是高速外部時鐘,可接石英/陶瓷諧振器,或者接外部時鐘源,頻率範圍為 4MHz~16MHz。

我們的開發板接的是 8M 的晶振。

③、LSI 是低速內部時鐘,RC 振蕩器,頻率為 40kHz。獨立看門狗的時鐘源只能是 LSI,同

時 LSI 還可以作為 RTC 的時鐘源。

④、LSE 是低速外部時鐘,接頻率為 32.768kHz 的石英晶體。這個主要是 RTC 的時鐘源。

⑤、PLL 為鎖相環倍頻輸出,其時鐘輸入源可選擇為 HSI/2、HSE 或者 HSE/2。倍頻可選擇為

2~16 倍,但是其輸出頻率最大不得超過 72MHz。

上面我們簡要概括了 STM32 的時鐘源,那麼這 5 個時鐘源是怎麼給各個外設以及系統提

供時鐘的呢?這裡我們將一一講解。我們還是從圖的下方講解起吧,因為下方比較簡單。

圖中我們用 A~E 標示我們要講解的地方。

A. MCO 是 STM32 的一個時鐘輸出 IO(PA8),它可以選擇一個時鐘信號輸出,可以

選擇為 PLL 輸出的 2 分頻、HSI、HSE、或者系統時鐘。這個時鐘可以用來給外

部其他系統提供時鐘源。

B. 這裡是 RTC 時鐘源,從圖上可以看出,RTC 的時鐘源可以選擇 LSI,LSE,以及

HSE 的 128 分頻。

C. 從圖中可以看出 C 處 USB 的時鐘是來自 PLL 時鐘源。STM32 中有一個全速功能

的 USB 模塊,其串行接口引擎需要一個頻率為 48MHz 的時鐘源。該時鐘源只能

從 PLL 輸出端獲取,可以選擇為 1.5 分頻或者 1 分頻,也就是,當需要使用 USB

模塊時,PLL 必須使能,並且時鐘頻率配置為 48MHz 或 72MHz。

D. D 處就是 STM32 的系統時鐘 SYSCLK,它是供 STM32 中絕大部分部件工作的時

鍾源。系統時鐘可選擇為 PLL 輸出、HSI 或者 HSE。系統時鐘最大頻率為 72MHz,

當然你也可以超頻,不過一般情況為了系統穩定性是沒有必要冒風險去超頻的。

E. 這裡的 E 處是指其他所有外設了。從時鐘圖上可以看出,其他所有外設的時鐘最

終來源都是 SYSCLK。SYSCLK 通過 AHB 分頻器分頻後送給各模塊使用。這些模塊包

括:

①、AHB 總線、內核、內存和 DMA 使用的 HCLK 時鐘。

②、通過 8 分頻後送給 Cortex 的系統定時器時鐘,也就是 systick 了。

③、直接送給 Cortex 的空閒運行時鐘 FCLK。

④、送給 APB1 分頻器。APB1 分頻器輸出一路供 APB1 外設使用(PCLK1,最大

頻率 36MHz),另一路送給定時器(Timer)2、3、4 倍頻器使用。

⑤、送給 APB2 分頻器。APB2 分頻器分頻輸出一路供 APB2 外設使用(PCLK2,

最大頻率 72MHz),另一路送給定時器(Timer)1 倍頻器使用。

其中需要理解的是 APB1 和 APB2 的區別,APB1 上面連接的是低速外設,包括電源接口、

備份接口、CAN、USB、I2C1、I2C2、UART2、UART3 等等,APB2 上面連接的是高速外設包

括 UART1、SPI1、Timer1、ADC1、ADC2、所有普通 IO 口(PA~PE)、第二功能 IO 口等。居寧

老師的《稀裡糊塗玩 STM32》資料裡面教大家的記憶方法是 2>1, APB2 下面所掛的外設的時

鍾要比 APB1 的高。

在以上的時鐘輸出中,有很多是帶使能控制的,例如 AHB 總線時鐘、內核時鐘、各種 APB1

外設、APB2 外設等等。當需要使用某模塊時,記得一定要先使能對應的時鐘。後面我們講解

實例的時候回講解到時鐘使能的方法。

4.3.2 STM32F103 時鐘系統配置

上一小節我們對 STM32F103 時鐘樹進行了詳細講解,接下來我們來講解通過 STM32F1 的

HAL 庫進行 STM32F103 時鐘系統配置步驟。實際上,STM32F1 的時鐘系統配置也可以通過圖

形化配置工具 STM32CubeMX 來配置生成,這裡我們講解初始化代碼,是為了讓大家對 STM32

時鐘系統有更加清晰的理解。

前面我們講解過,在系統啟動之後,程序會先執行 HAL 庫定義的 SystemInit 函數,進行系

統一些初始化配置。那麼我們先來看看 SystemInit 程序:

void SystemInit (void)

{

/* 將 RCC 時鐘配置重置為默認重置狀態(用於調試)*/

RCC->CR |= (uint32_t)0x00000001; //打開 HSION 位

/* 設置 SW, HPRE, PPRE1, PPRE2, ADCPRE 和 MCO 位 */

#if !defined(STM32F105xC) && !defined(STM32F107xC)

RCC->CFGR &= (uint32_t)0xF8FF0000;

#else

RCC->CFGR &= (uint32_t)0xF0FF0000;

#endif /* STM32F105xC */

RCC->CR &= (uint32_t)0xFEF6FFFF; // 復位 HSEON, CSSON 和 PLLON 位

RCC->CR &= (uint32_t)0xFFFBFFFF; // 復位 HSEBYP 位

RCC->CFGR &= (uint32_t)0xFF80FFFF; //復位 CFGR 寄存器

#if defined(STM32F105xC) || defined(STM32F107xC)

RCC->CR &= (uint32_t)0xEBFFFFFF; // 復位 PLL2ON 和 PLL3ON 位

RCC->CIR = 0x00FF0000; // 禁用所有中斷並清除掛起位

RCC->CFGR2 = 0x00000000; // 重置 CFGR2 註冊

#elif defined(STM32F100xB) || defined(STM32F100xE)

RCC->CIR = 0x009F0000; // 禁用所有中斷並清除掛起位

RCC->CFGR2 = 0x00000000; // 重置 CFGR2 註冊

#else

RCC->CIR = 0x009F0000; // 禁用所有中斷並清除掛起位

#endif /* STM32F105xC */

#if defined(STM32F100xE) || defined(STM32F101xE) || defined(STM32F101xG) ||

defined(STM32F103xE) || defined(STM32F103xG)

#ifdef DATA_IN_ExtSRAM

SystemInit_ExtMemCtl();

#endif /* DATA_IN_ExtSRAM */

#endif

/* 配置中斷向量表地址=基地址+偏移地址 ------------------*

#ifdef VECT_TAB_SRAM

SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; //內部 SRAM 中的向量表重定位

#else

SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; //在內部 FLASH 中的向量表重定位

#endif

}從上面代碼可以看出,SystemInit 主要做了如下三個方面工作:

1) 復位 RCC 時鐘配置為默認復位值(默認開始了 HIS)

2) 外部存儲器配置

3) 中斷向量表地址配置

HAL 庫的 SystemInit 函數並沒有像標準庫的 SystemInit 函數一樣進行時鐘的初始化配置。HAL

庫的 SystemInit 函數除了打開 HSI 之外,沒有任何時鐘相關配置,所以使用 HAL 庫我們必須編

寫自己的時鐘配置函數。首先我們打開工程模板看看我們在工程 SYSTEM 分組下面定義的 sys.c

文件中的時鐘初始化函數 Stm32_Clock_Init 的內容:

//時鐘系統配置函數

//PLL:選擇的倍頻數,RCC_PLL_MUL2~RCC_PLL_MUL16

//返回值:0,成功;1,失敗

void Stm32_Clock_Init(u32 PLL)

{

HAL_StatusTypeDef ret = HAL_OK;

RCC_OscInitTypeDef RCC_OscInitStructure;

RCC_ClkInitTypeDef RCC_ClkInitStructure;

RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //時鐘源為 HSE

RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打開 HSE

RCC_OscInitStructure.HSEPredivValue=RCC_HSE_PREDIV_DIV1; //HSE 預分頻

RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON; //打開 PLL

RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE;

//PLL 時鐘源選擇 HSE

RCC_OscInitStructure.PLL.PLLMUL=PLL; //主 PLL 倍頻因子

ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化

if(ret!=HAL_OK) while(1);

//選中 PLL 作為系統時鐘源並且配置 HCLK,PCLK1 和 PCLK2

RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|

RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|

RCC_CLOCKTYPE_PCLK2);

//設置系統時鐘時鐘源為 PLL

RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;

RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1; //AHB 分頻係數為 1

RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV2; //APB1 分頻係數為 2

RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV1; //APB2 分頻係數為 1

ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_2);

//同時設置 FLASH 延時周期為 2WS,也就是 3 個 CPU 周期。

if(ret!=HAL_OK) while(1);

}

從函數注釋可知,函數 Stm32_Clock_Init 的作用是進行時鐘系統配置,除了配置 PLL 相關

參數確定 SYSCLK 值之外,還配置了 AHB,APB1 和 APB2 的分頻係數,也就是確定了 HCLK,

PCLK1 和 PCLK2 的時鐘值。

接下來我們看看結構體 RCC_OscInitTypeDef 的定義:

typedef struct

{

uint32_t OscillatorType; //需要選擇配置的振蕩器類型

uint32_t HSEState; //HSE 狀態

uint32_t HSEPredivValue; // Prediv1 值

uint32_t LSEState; //LSE 狀態

uint32_t HSIState; //HIS 狀態

uint32_t HSICalibrationValue; //HIS 校準值

uint32_t LSIState; //LSI 狀態

RCC_PLLInitTypeDef PLL; //PLL 配置

}RCC_OscInitTypeDef;

對於這個結構體,前面幾個參數主要是用來選擇配置的振蕩器類型。比如我們要開啟 HSE,

那麼我們會設置 OscillatorType 的值為 RCC_OSCILLATORTYPE_HSE,然後設置 HSEState 的值

為 RCC_HSE_ON 開啟 HSE。對於其他時鐘源 HSI,LSI 和 LSE,配置方法類似。這個結構體還

有一個很重要的成員變量是 PLL,它是結構體 RCC_PLLInitTypeDef 類型。它的作用是配置 PLL

相關參數,我們來看看它的定義:

typedef struct

{

uint32_t PLLState; //PLL 狀態

uint32_t PLLSource; //PLL 時鐘源

uint32_t PLLMUL; //PLL VCO 輸入時鐘的乘法因子

}RCC_PLLInitTypeDef;

從 RCC_PLLInitTypeDef;結構體的定義很容易看出該結構體主要用來設置 PLL 時鐘源以及

相關分頻倍頻參數。

這個結構體的定義我們就不做過多講解,接下來我們看看我們的時鐘初始化函數

Stm32_Clock_Init 中的配置內容:

RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //時鐘源為 HSE

RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打開 HSE

RCC_OscInitStructure.HSEPredivValue=RCC_HSE_PREDIV_DIV1; //HSE 預分頻

RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON; //打開 PLL

RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE;

//PLL 時鐘源選擇 HSE

RCC_OscInitStructure.PLL.PLLMUL=PLL; //主 PLL 倍頻因子

ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化

通過該段函數,我們開啟了 HSE 時鐘源,同時選擇 PLL 時鐘源為 HSE,然後把

Stm32_Clock_Init 的唯一的入口參數直接設置作為 PLL 的倍頻因子。設置好 PLL 時鐘源參數之

後,也就是確定了 PLL 的時鐘頻率,接下來我們就需要設置系統時鐘,以及 AHB,APB1 和

APB2 相關參數。

接下來我們來看看步驟 5 中提到的 HAL_RCC_ClockConfig()函數,聲明如下:

HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct,

uint32_t FLatency);

該函數有兩個入口參數,第一個入口參數 RCC_ClkInitStruct 是結構體 RCC_ClkInitTypeDef

指針類型,用來設置 SYSCLK 時鐘源以及 AHB,APB1 和 APB2 的分頻係數。第二個入口參數

FLatency 用來設置 FLASH 延遲,這個參數我們放在後面講解。

RCC_ClkInitTypeDef 結構體類型定義非常簡單,這裡我們就不列出來,我們來看看

Stm32_Clock_Init 函數中的配置內容:

//選中 PLL 作為系統時鐘源並且配置 HCLK,PCLK1 和 PCLK2

RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|

RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|

RCC_CLOCKTYPE_PCLK2);

//設置系統時鐘時鐘源為 PLL

RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;

RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1; //AHB 分頻係數為 1

RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV2; //APB1 分頻係數為 2

RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV1; //APB2 分頻係數為 1

ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_2);

//同時設置 FLASH 延時周期為 2WS,也就是 3 個 CPU 周期。

第一個參數 ClockType 配置說明我們要配置的是 SYSCLK,HCLK,PCLK1 和 PCLK2 四個時鐘。

第二個參數 SYSCLKSource 配置選擇系統時鐘源為 PLL。

第三個參數 AHBCLKDivider 配置 AHB 分頻係數為 1。

第四個參數 APB1CLKDivider 配置 APB1 分頻係數為 2。

第五個參數 APB2CLKDivider 配置 APB2 分頻係數為 1。

根據我們在主函數中調用 Stm32_Clock_Init(RCC_PLL_MUL9)時候設置的入口參數值,我

們可以計算出,PLL 時鐘為 PLLCLK=HSE*9 =8MHz*9=72MHz,同時我們選擇系統時鐘源為

PLL , 所 以 系 統 時 鍾 SYSCLK=72MHz 。 AHB 分頻系 數 為 1 ,故其頻率為

HCLK=SYSCLK/1=72MHz。APB1 分頻係數為 2,故其頻率為 PCLK1=HCLK/2=36MHz。APB2

分頻係數為 1,故其頻率為 PCLK2=HCLK/1=72/1=72MHz。最後我們總結一下通過調用函數

Stm32_Clock_Init(RCC_PLL_MUL9)之後的關鍵時鐘頻率值:

SYSCLK(系統時鐘) =72MHz

PLL 主時鐘 =72MHz

AHB 總線時鐘(HCLK=SYSCLK/1) =72MHz

APB1 總線時鐘(PCLK1=HCLK/2) =36MHz

APB2 總線時鐘(PCLK2=HCLK/1) =72MHz

4.3.3 STM32F1 時鐘使能和配置

上一節我們講解了時鐘系統配置步驟。在配置好時鐘系統之後,如果我們要使用某些外設,

例如 GPIO,ADC 等,我們還要使能這些外設時鐘。這裡大家必須注意,如果在使用外設之前

沒有使能外設時鐘,這個外設是不可能正常運行的。STM32 的外設時鐘使能是在 RCC 相關寄

存器中配置的。因為 RCC 相關寄存器非常多,有興趣的同學可以直接打開《STM32 中文參考手

冊 V10》6.3 小節查看所有 RCC 相關寄存器的配置。接下來我們來講解通過 STM32F1 的 HAL

庫使能外設時鐘的方法。

在 STM32F1 的 HAL 庫中,外設時鐘使能操作都是在 RCC 相關固件庫文件頭文件

stm32f1xx_hal_rcc.h 定義的。大家打開 stm32f1xx_hal_rcc.h 頭文件可以看到文件中除了少數幾

個函數聲明之外大部分都是宏定義標識符。外設時鐘使能在 HAL 庫中都是通過宏定義標識符

來實現的。首先,我們來看看 GPIOA 的外設時鐘使能宏定義標識符:

#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \

__IO uint32_t tmpreg; \

SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);\

tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);\

UNUSED(tmpreg); \

} while(0U))

這幾行代碼非常簡單,主要是定義了一個宏定義標識符__HAL_RCC_GPIOA_CLK_ENABLE(),

它的核心操作是通過下面這行代碼實現的:

SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);

這行代碼的作用是,設置寄存器 RCC_APB2ENR 的相關位為 1,至於是哪個位,是由宏定義標

識符 RCC_APB2ENR_IOPAEN 的值決定的,而它的值為:

#define RCC_APB2ENR_IOPAEN ((uint32_t)0x00000001)

所以,我們很容易理解上面代碼的作用是設置寄存器 RCC->APB2ENR 寄存器的位 2 為 1。我

們可以從 STM32F1 的中文參考手冊中搜索 APB2ENR 寄存器定義,位 2 的作用是用來使用

GPIOA 時鐘。APB2ENR 寄存器的位 2 描述如下:

位 2 IOPAEN:IO 埠 A 時鐘使能

由軟體置 1 和清零

0:禁止 IO 埠 A 時鐘

1:使能 IO 埠 A 時鐘

那麼我們只需要在我們的用戶程序中調用宏定義標識符__HAL_RCC_GPIOA_CLK_ENABLE()

就可以實現 GPIOA 時鐘使能。使用方法為:

__HAL_RCC_GPIOA_CLK_ENABLE();//使能 GPIOA 時鐘

對於其他外設,同樣都是在 stm32f1xx_hal_rcc.h 頭文件中定義,大家只需要找到相關宏定義標

識符即可,這裡我們列出幾個常用使能外設時鐘的宏定義標識符使用方法:

__HAL_RCC_DMA1_CLK_ENABLE();//使能 DMA1 時鐘

__HAL_RCC_USART2_CLK_ENABLE();//使能串口 2 時鐘

__HAL_RCC_TIM1_CLK_ENABLE();//使能 TIM1 時鐘

我們使用外設的時候需要使能外設時鐘,如果我們不需要使用某個外設,同樣我們可以禁

止某個外設時鐘。禁止外設時鐘使用方法和使能外設時鐘非常類似,同樣是頭文件中定義的宏

定義標識符。我們同樣以 GPIOA 為例,宏定義標識符為:

#define __HAL_RCC_GPIOA_CLK_DISABLE() \

(RCC->APB2ENR &= ~(RCC_APB2ENR_IOPAEN))

同樣,宏定義標識符__HAL_RCC_GPIOA_CLK_DISABLE()的作用是設置 RCC->APB2ENR 寄

存器的位 2 為 0,也就是禁止 GPIOA 時鐘。具體使用方法我們這裡就不做過多講解,我們這裡

同樣列出幾個常用的禁止外設時鐘的宏定義標識符使用方法:

__HAL_RCC_DMA1_CLK_DISABLE();//禁止 DMA1 時鐘

__HAL_RCC_USART2_CLK_DISABLE();//禁止串口 2 時鐘

__HAL_RCC_TIM1_CLK_DISABLE();//禁止 TIM1 時鐘

關於 STM32F1 的外設時鐘使能和禁止方法我們就給大家講解到這裡。

4.4 埠復用和重映射

STM32F1 有很多的內置外設,這些外設的外部引腳都是與 GPIO 復用的。也就是說,一個 GPIO

如果可以復用為內置外設的功能引腳,那麼當這個 GPIO 作為內置外設使用的時候,就叫做復用。

這部分知識在《STM32 中文參考手冊 V10》的 P109,P116~P121 有詳細的講解哪些 GPIO 管腳是

可以復用為哪些內置外設的。這裡我們就不一一講解。

大家都知道,MCU 都有串口,STM32 有好幾個串口。比如說 STM32F103ZET6 有 5 個串口,我

們可以查手冊知道,串口 1 的引腳對應的 IO 為 PA9,PA10.PA9,PA10 默認功能是 GPIO,所以當

PA9,PA10 引腳作為串口 1 的 TX,RX 引腳使用的時候,那就是埠復用。

圖 4.4.1.1 串口 1 復用管腳

接下來我們以串口 1 為例來講解配置 GPOPA.9,GPIOA.10 口為串口 1 復用功能的一般步驟。

① 首先,我們要使用 IO 復用功能,必須先打開對應的 IO 時鐘和復用功能外設時鐘,這裡

我們使用了 GPIOA 以及 USART1,所以我們需要使能 GPIOA 和 USART1 時鐘。方法如下:

__HAL_RCC_GPIOA_CLK_ENABLE(); //使能 GPIOA 時鐘

__HAL_RCC_USART1_CLK_ENABLE(); //使能 USART1 時鐘

__HAL_RCC_AFIO_CLK_ENABLE(); //使能輔助功能 IO 時鐘

② 然後,我們在 GIPOx_MODER 寄存器中將所需 IO(對於串口 1 是 PA9,PA10)配置為復

用功能。

③ 最後,我們還需要對 IO 口的其他參數,例如上拉/下拉以及輸出速度等進行配置。

上面三步,在我們 HAL 庫中是通過 HAL_GPIO_Init 函數來實現的,參考代碼如下:

GPIO_InitTypeDef GPIO_Initure;

GPIO_Initure.Pin=GPIO_PIN_9; //PA9

GPIO_Initure.Mode=GPIO_MODE_AF_PP; //復用推輓輸出

GPIO_Initure.Pull=GPIO_PULLUP; //上拉

GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;//高速

HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9

通過上面的配置,PA9 復用為串口 1 的發送引腳。這個時候,PA9 將不再作為普通的 IO 口

使用。對於 PA10,配置方法一樣,修改 Pin 成員變量值為 PIN_10 即可。

STM32F1 的埠復用和映射就給大家講解到這裡,希望大家課餘結合相關實驗工程和手冊

鞏固本小節知識。

4.5 STM32 NVIC 中斷優先級管理

CM3 內核支持 256 個中斷,其中包含了 16 個內核中斷和 240 個外部中斷,並且具有 256

級的可編程中斷設置。但 STM32 並沒有使用 CM3 內核的全部東西,而是只用了它的一部分。

STM32 有 84 個中斷,包括 16 個內核中斷和 68 個可屏蔽中斷,具有 16 級可編程的中斷優先級。

而我們常用的就是這 68 個可屏蔽中斷,但是 STM32 的 68 個可屏蔽中斷,在 STM32F103 系列

上面,又只有 60 個(在 107 系列才有 68 個)。因為我們開發板選擇的晶片是 STM32F103 系列

的所以我們就只針對 STM32F103 系列這 60 個可屏蔽中斷進行介紹。

在 MDK 內,與 NVIC 相關的寄存器,MDK 為其定義了如下的結構體:

typedef struct

{

__IOM uint32_t ISER[8U];

uint32_t RESERVED0[24U];

__IOM uint32_t ICER[8U];

uint32_t RSERVED1[24U];

__IOM uint32_t ISPR[8U];

uint32_t RESERVED2[24U];

__IOM uint32_t ICPR[8U];

uint32_t RESERVED3[24U];

__IOM uint32_t IABR[8U];

uint32_t RESERVED4[56U];

__IOM uint8_t IP[240U];

uint32_t RESERVED5[644U];

__OM uint32_t STIR;

} NVIC_Type;;

STM32 的中斷在這些寄存器的控制下有序的執行的。只有了解這些中斷寄存器,才能方便

的使用 STM32 的中斷。下面重點介紹這幾個寄存器:

ISER[8]:ISER 全稱是:Interrupt Set-Enable Registers,這是一個中斷使能寄存器組。上面

說了 CM3 內核支持 256 個中斷,這裡用 8 個 32 位寄存器來控制,每個位控制一個中斷。但是

STM32F103 的可屏蔽中斷只有 60 個,所以對我們來說,有用的就是兩個(ISER[0]和 ISER[1]),

總共可以表示 64 個中斷。而 STM32F103 只用了其中的前 60 位。ISER[0]的 bit0~bit31 分別對

應中斷 0~31。ISER[1]的 bit0~27 對應中斷 32~59;這樣總共 60 個中斷就分別對應上了。你要

使能某個中斷,必須設置相應的 ISER 位為 1,使該中斷被使能(這裡僅僅是使能,還要配合中

斷分組、屏蔽、IO 口映射等設置才算是一個完整的中斷設置)。具體每一位對應哪個中斷,請

參考 stm32f10x.h 裡面的第 140 行處(針對編譯器 MDK5 來說)。

ICER[8]:全稱是:Interrupt Clear-Enable Registers,是一個中斷除能寄存器組。該寄存器組

與 ISER 的作用恰好相反,是用來清除某個中斷的使能的。其對應位的功能,也和 ICER 一樣。

這裡要專門設置一個 ICER 來清除中斷位,而不是向 ISER 寫 0 來清除,是因為 NVIC 的這些寄

存器都是寫 1 有效的,寫 0 是無效的。具體為什麼這麼設計,請看《CM3 權威指南》第 125 頁,

NVIC 概覽一章。

ISPR[8]:全稱是:Interrupt Set-Pending Registers,是一個中斷掛起控制寄存器組。每個位

對應的中斷和 ISER 是一樣的。通過置 1,可以將正在進行的中斷掛起,而執行同級或更高級別

的中斷。寫 0 是無效的。

ICPR[8]:全稱是:Interrupt Clear-Pending Registers,是一個中斷解掛控制寄存器組。其作

用與 ISPR 相反,對應位也和 ISER 是一樣的。通過設置 1,可以將掛起的中斷接掛。寫 0 無效。

IABR[8]:全稱是:Interrupt Active Bit Registers,是一個中斷激活標誌位寄存器組。對應位

所代表的中斷和 ISER 一樣,如果為 1,則表示該位所對應的中斷正在被執行。這是一個只讀寄

存器,通過它可以知道當前在執行的中斷是哪一個。在中斷執行完了由硬體自動清零。

IP[240]:全稱是:Interrupt Priority Registers,是一個中斷優先級控制的寄存器組。這個寄

存器組相當重要!STM32 的中斷分組與這個寄存器組密切相關。IP 寄存器組由 240 個 8bit 的寄

存器組成,每個可屏蔽中斷佔用 8bit,這樣總共可以表示 240 個可屏蔽中斷。而 STM32 只用到

了其中的前 60 個。IP[59]~IP[0]分別對應中斷 59~0。而每個可屏蔽中斷佔用的 8bit 並沒有全部

使用,而是 只用了高 4 位。這 4 位,又分為搶佔優先級和子優先級。搶佔優先級在前,子優先

級在後。而這兩個優先級各佔幾個位又要根據 SCB->AIRCR 中的中斷分組設置來決定。

這裡簡單介紹一下 STM32 的中斷分組:STM32 將中斷分為 5 個組,組 0~4。該分組的設

置是由 SCB->AIRCR 寄存器的 bit10~8 來定義的。具體的分配關係如表 4.5.1 所示:

表 4.5.1 AIRCR 中斷分組設置表

通過這個表,我們就可以清楚的看到組 0~4 對應的配置關係,例如組設置為 3,那麼此時

所有的 60 個中斷,每個中斷的中斷優先寄存器的高四位中的最高 3 位是搶佔優先級,低 1 位是

響應優先級。每個中斷,你可以設置搶佔優先級為 0~7,響應優先級為 1 或 0。搶佔優先級的

級別高於響應優先級。而數值越小所代表的優先級就越高。

這裡需要注意兩點:第一,如果兩個中斷的搶佔優先級和響應優先級都是一樣的話,則看

哪個中斷先發生就先執行;第二,高優先級的搶佔優先級是可以打斷正在進行的低搶佔優先級

中斷的。而搶佔優先級相同的中斷,高優先級的響應優先級不可以打斷低響應優先級的中斷。

結合實例說明一下:假定設置中斷優先級組為 2,然後設置中斷 3(RTC 中斷)的搶佔優先級

為 2,響應優先級為 1。中斷 6(外部中斷 0)的搶佔優先級為 3,響應優先級為 0。中斷 7(外

部中斷 1)的搶佔優先級為 2,響應優先級為 0。那麼這 3 個中斷的優先級順序為:中斷 7>中

斷 3>中斷 6。

上面例子中的中斷 3 和中斷 7 都可以打斷中斷 6 的中斷。而中斷 7 和中斷 3 卻不可以相互

打斷!

通過以上介紹,我們熟悉了 STM32F103 中斷設置的大致過程。接下來我們介紹如何使用

HAL 庫實現以上中斷分組設置以及中斷優先級管理,使中斷配置簡單化。NVIC 中斷管理相關

函數主要在 HAL 庫關鍵文件 stm32f1xx_hal_cortex.c 中定義。

首先要講解的是中斷優先級分組函數 HAL_NVIC_SetPriorityGrouping,其函數申明如下:

void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);

這個函數的作用是對中斷的優先級進行分組,這個函數在系統中只需要被調用一次,一旦

分組確定就最好不要更改,否則容易造成程序分組混亂。這個函數我們可以找到其函數體內容

如下:

void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup)

{

/* Check the parameters */

assert_param(IS_NVIC_PRIORITY_GROUP(PriorityGroup));

/* Set the PRIGROUP[10:8] bits according to the PriorityGroup parameter value */

NVIC_SetPriorityGrouping(PriorityGroup);

}

從函數體以及注釋可以看出,這個函數是通過調用函數 NVIC_SetPriorityGrouping 來進行中斷

優先級分組設置。通過查找(參考 3.5.3 小節 MDK 中「Go to definition of」的使用方法),我們可

以知道函數 NVIC_SetPriorityGrouping 是在文件 core_cm3.h 頭文件中定義的。接下來,我們來

分析一下函數 NVIC_SetPriorityGrouping 函數定義。定義如下:

__STATIC_INLINE void NVIC_SetPriorityGrouping(uint32_t PriorityGroup)

{

uint32_t reg_value;

uint32_t PriorityGroupTmp = (PriorityGroup & (uint32_t)0x07UL);

reg_value= SCB->AIRCR; /* read old register configuration */

reg_value&=~((uint32_t)(SCB_AIRCR_VECTKEY_Msk |SCB_AIRCR_PRIGROUP_Msk));

reg_value = (reg_value|((uint32_t)0x5FAUL << SCB_AIRCR_VECTKEY_Pos) |

(PriorityGroupTmp<< SCB_AIRCR_PRIGROUP_Pos) );

SCB->AIRCR = reg_value;

}

從函數內容可以看出,這個函數主要作用是通過設置 SCB->AIRCR 寄存器的值來設置中斷優先

級分組,這在前面寄存器講解的過程中已經講到。

關於函數 HAL_NVIC_SetPriorityGrouping 的函數體內容解讀我就給大家介紹到這裡。接下

來我們來看看這個函數的入口參數。大家繼續回到函數 HAL_NVIC_SetPriorityGrouping 的定義

可以看到,函數的最開頭有這樣一行函數:

assert_param(IS_NVIC_PRIORITY_GROUP(PriorityGroup));

其中函數 assert_param 是斷言函數,它的作用主要是對入口參數的有效性進行判斷。也就是說

我們可以通過這個函數知道入口參數在哪些範圍內是有效的。而其入口參數通過在 MDK 中雙

擊選中 「IS_NVIC_PRIORITY_GROUP」,然後右鍵「Go to defition of …」可以查看到為:

#define IS_NVIC_PRIORITY_GROUP(GROUP)

(((GROUP) == NVIC_PriorityGroup_0) ||\

((GROUP) == NVIC_PriorityGroup_1) || \

((GROUP) == NVIC_PriorityGroup_2) || \

((GROUP) == NVIC_PriorityGroup_3) || \

((GROUP) == NVIC_PriorityGroup_4))

從這個內容可以看出,當 GROUP 的值為 NVIC_PriorityGroup_0~ NVIC_PriorityGroup_4 的時候,

IS_NVIC_PRIORITY_GROUP 的值才為真。這也就是我們上面表 4.5.1 講解的,分組範圍為 0-4,

對應的入口參數為宏定義值 NVIC_PriorityGroup_0~ NVIC_PriorityGroup_4。比如我們設置整個

系統的中斷優先級分組值為 2,那麼方法是:

HAL_NVIC_SetPriorityGrouping (NVIC_PriorityGroup_2);

這樣就確定了中斷優先級分組為 2,也就是 2 位搶佔優先級,2 位響應優先級,搶佔優先級和響

應優先級的值的範圍均為 0-3。

講到這裡,大家對怎麼進行系統的中斷優先級分組設置,以及具體的中斷優先級設置函數

HAL_NVIC_SetPriorityGrouping 的內部函數實現都有了一個詳細的理解。接下來我們來看看在

HAL 庫裡面,是怎樣調用 HAL_NVIC_SetPriorityGrouping 函數進行分組設置的。

打開 stm32f1xx_hal.c 文件可以看到,文件內部定義了 HAL 庫初始化函數 HAL_Init,這個

函數非常重要,其作用主要是對中斷優先級分組,FLASH 以及硬體層進行初始化,我們在 3.1

小節對其進行了比較詳細的講解。這裡我們只需要知道,在系統主函數 main 開頭部分,我們都

會首先調用 HAL_Init 函數進行一些初始化操作。在 HAL_Init 內部,有如下一行代碼:

HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

這行代碼的作用是把系統中斷優先級分組設置為分組 4,這在我們前面已經詳細講解。也

就是說,在主函數中調用 HAL_Init 函數之後,在 HAL_Init 函數內部會通過調用我們前面講解

的 HAL_NVIC_SetPriorityGrouping 函數來進行系統中斷優先級分組設置。所以,我們要進行中

斷優先級分組設置,只需要修改 HAL_Init 函數內部的這行代碼即可。中斷優先級分組的內容我

們就給大家講解到這裡。

設置好了系統中斷分組,也就是確定了那麼對於每個中斷我們又怎麼確定他的搶佔優先級

和響應優先級呢?官方 HAL 庫文件 stm32f1xx_hal_cortex.c 中定義了三個單個中斷優先級設置

函數。函數如下:

void HAL_NVIC_SetPriority(IRQn_Type IRQn,

uint32_t PreemptPriority, uint32_t SubPriority);

void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);

void HAL_NVIC_DisableIRQ(IRQn_Type IRQn);

第一個函數 HAL_NVIC_SetPriority 是用來設置單個優先級的搶佔優先級和響應優先級的值。

第二個函數 HAL_NVIC_EnableIRQ 是用來使能某個中斷通道。

第三個函數 HAL_NVIC_DisableIRQ 是用來清除某個中斷使能的,也就是中斷失能。

這三個函數的使用都非常簡單,對於具體的調用方法,大家可以參考我們後面第九章外部中斷

實驗講解。

這裡大家還需要注意,中斷優先級分組和中斷優先級設置是兩個不同的概念。中斷優先級

分組是用來設置整個系統對於中斷分組設置為哪個分組,分組號為 0-4,設置函數為

HAL_NVIC_SetPriorityGrouping,確定了中斷優先級分組號,也就確定了系統對於單個中斷的

搶佔優先級和響應優先級設置各佔幾個位(對應表 4.5.1)。設置好中斷優先級分組,確定了分

組號之後,接下來我們就是要對單個優先級進行中斷優先級設置。也就是這個中斷的搶佔優先

級和響應優先級的值,設置方法就是我們上面講解的三個函數。

最後我們總結一下中斷優先級設置的步驟:

①系統運行開始的時候設置中斷分組。確定組號,也就是確定搶佔優先級和響應優先級的

分配位數。設置函數為 HAL_NVIC_PriorityGroupConfig。對於 HAL 庫,在文件 stm32f1xx_hal.c

內部定義函數 HAL_Init 中有調用 HAL_NVIC_PriorityGroupConfig 函數進行相關設置,所以我

們只需要修改 HAL_Init 內部對中斷優先級分組設置即可。

② 設置單個中斷的中斷優先級別和使能相應中斷通道,使用到的函數函數主要為函數

HAL_NVIC_SetPriority 和函數 HAL_NVIC_EnableIRQ。

4.6 HAL 庫中寄存器地址名稱映射分析

之所以要講解這部分知識,是因為經常會遇到客戶提到不明白 HAL 庫中那些結構體是怎麼

與寄存器地址對應起來的。這裡我們就做一個簡要的分析吧。

首先我們看看 51 中是怎麼做的。51 單片機開發中經常會引用一個 reg51.h 的頭文件,下

面我們看看他是怎麼把名字和寄存器聯繫起來的:

sfr P0 =0x80;

sfr 也是一種擴充數據類型,點用一個內存單元,值域為 0~255。利用它可以訪問 51 單片

機內部的所有特殊功能寄存器。如用 sfr P1 = 0x90 這一句定義 P1 為 P1 埠在片內的寄存

器。然後我們往地址為 0x80 的寄存器設值的方法是:P0=value;

那麼在 STM32 中,是否也可以這樣做呢??答案是肯定的。肯定也可以通過同樣的方

式來做,但是 STM32 因為寄存器太多太多,如果一一以這樣的方式列出來,那要好大的篇

幅,既不方便開發,也顯得太雜亂無序的感覺。所以 MDK 採用的方式是通過結構體來將

寄存器組織在一起。下面我們就講解 MDK 是怎麼把結構體和地址對應起來的,為什麼我

們修改結構體成員變量的值就可以達到操作對應寄存器的值。這些事情都是在 stm32f1xx.h

文件中完成的。我們通過 GPIOA 的幾個寄存器的地址來講解吧。

首先我們可以查看《STM32 中文參考手冊 V10》中的寄存器地址映射表(P129)。這裡

我們選用 GPIOA 為例來講解。GPIO 寄存器地址映射如下表 4.6.1:

表 4.6.1 GPIO 寄存器地址映射表

從這個表我們可以看出,GPIOA 的 7 個寄存器都是 32 位的,所以每個寄存器佔有 4

個地址,一共佔用 28 個地址,地址偏移範圍為(000h~01Bh)。這個地址偏移是相對 GPIOA

的基地址而言的。GPIOA 的基地址是怎麼算出來的呢?因為 GPIO 都是掛載在 APB2 總線

之上,所以它的基地址是由 APB2 總線的基地址+GPIOA 在 APB2 總線上的偏移地址決定

的。同理依次類推,我們便可以算出 GPIOA 基地址了。下面我們打開 stm32f103.h 定位到

GPIO_TypeDef 定義處:

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;

然後定位到:

#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)

可以看出,GPIOA 是將 GPIOA_BASE 強制轉換為 GPIO_TypeDef 結構體指針,這句話的

意思是,GPIOA 指向地址 GPIOA_BASE,GPIOA_BASE 存放的數據類型為 GPIO_TypeDef。

然後在 MDK 中雙擊「GPIOA_BASE」選中之後右鍵選中「Go to definition of 」,便可以查

看 GPIOA_BASE 的宏定義:

#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)

依次類推,可以找到最頂層:

#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)

#define PERIPH_BASE ((uint32_t)0x40000000)

所以我們便可以算出 GPIOA 的基地址位:

GPIOA_BASE= 0x40000000+0x10000+0x0800=0x40010800

下面我們再跟《STM32 中文參考手冊 V10》比較一下看看 GPIOA 的基地址是不是

0x40010800。截圖 P28 存儲器映射表我們可以看到,GPIOA 的起始地址也就是基

地址確實是 0x40010800:

圖 4.6.2 GPIO 存儲器地址映射表

同樣的道理,我們可以推算出其他外設的基地址。

上面我們已經知道 GPIOA 的基地址,那麼那些 GPIOA 的 7 個寄存器的地址又是怎麼

算出來的呢??在上面我們講過 GPIOA 的各個寄存器對於 GPIOA 基地址的偏移地址,所

以我們自然可以算出來每個寄存器的地址。

GPIOA 的寄存器的地址=GPIOA 基地址+寄存器相對 GPIOA 基地址的偏移值

這個偏移值在上面的寄存器地址映像表中可以查到。

那麼在結構體裡面這些寄存器又是怎麼與地址一一對應的呢?這裡就涉及到結構體的

一個特徵,那就是結構體存儲的成員他們的地址是連續的。上面講到 GPIOA 是指向

GPIO_TypeDef 類型的指針,又由於 GPIO_TypeDef 是結構體,所以自然而然我們就可以算

出 GPIOA 指向的結構體成員變量對應地址了。

表 4.6.3 GPIOA 各寄存器實際地址表

我們可以把 GPIO_TypeDef 的定義中的成員變量的順序和 GPIOx 寄存器地址映像對比

可以發現,他們的順序是一致的,如果不一致,就會導致地址混亂了。

這就是為什麼固件庫裡面:GPIOA->BRR=value;就是設置地址為 0x40010800

+0x014(BRR 偏移量)=0x40010814 的寄存器 BRR 的值了。它和 51 裡面 P0=value 是設置地

址為 0x80 的 P0 寄存器的值是一樣的道理。

看到這裡你是否會學起來踏實一點呢??STM32 使用的方式雖然跟 51 單片機不一樣,

但是原理都是一致的。

4.7 MDK 中使用 HAL 庫快速組織代碼技巧

這一節主要講解在 MDK 中使用 HAL 庫開發的一些小技巧,僅供初學者參考。這節的知識

大家可以在學習第一個跑馬燈實驗的時候參考一下,對初學者應該很有幫助。我們就用最簡單

的 GPIO 初始化函數為例。

現 在 我 們 要 初 始 化 某 個 GPIO 端 口 , 我 們 要 怎 樣 快 速 操 作 呢 ? 在 頭 文 件

stm32f1xx_hal_gpio.h 頭文件中,聲明 GPIO 初始化函數為:

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);

現在我們想寫初始化函數,那麼我們在不參考其他代碼的前提下,怎麼快速組織代碼呢?

首先,我們可以看出,函數的入口參數是 GPIO_TypeDef 類型指針和 GPIO_InitTypeDef 類

型指針,因為 GPIO_TypeDef 入口參數比較簡單,所以我們就通過第二個入口參數

GPIO_InitTypeDef 類型指針來講解。雙擊 GPIO_InitTypeDef 後右鍵選擇「Go to definition of…」,

如下圖 4.7.1:

圖 4.7.1 查看類型定義方法

於是定位到 stm32f1xx_hal_gpio.h 中 GPIO_InitTypeDef 的定義處:

typedef struct

{

uint32_t Pin;

uint32_t Mode;

uint32_t Pull;

uint32_t Speed;

}GPIO_InitTypeDef;

可以看到這個結構體有 4 個成員變量,這也告訴我們一個信息,一個 GPIO 口的狀態是由模式

(Mode),速度(Speed)以及上下拉(Pull)來決定的。我們首先要定義一個結構體變量,下面

我們定義:

GPIO_InitTypeDef GPIO_InitStructure;

接著我們要初始化結構體變量 GPIO_InitStructure。首先我們要初始化成員變量 Pin,這個時候我

們就有點迷糊了,這個變量到底可以設置哪些值呢?這些值的範圍有什麼規定嗎?

這裡我們就回到 HAL_GPIO_Init 聲明處,同樣雙擊 HAL_GPIO_Init,右鍵點擊「Go to

definition of …」,這樣光標定位到 stm32f1xx_hal_gpio.c 文件中的 HAL_GPIO_Init 函數體開始處,

我們可以看到在函數中有如下幾行:

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)

…//此處省略部分代碼

assert_param(IS_GPIO_ALL_INSTANCE(GPIOx));

assert_param(IS_GPIO_PIN(GPIO_Init->Pin));

assert_param(IS_GPIO_MODE(GPIO_Init->Mode));

…//此處省略部分代碼

assert_param(IS_GPIO_PULL(GPIO_Init->Pull));

…//此處省略部分代碼

}

顧名思義,assert_param 是斷言語句,是對函數入口參數的有效性進行判斷,所以我們可以從

這個函數入手,確定入口參數範圍。第一行是對第一個參數 GPIOx 進行有效性判斷,雙擊

「IS_GPIO_ALL_INSTANCE」右鍵點擊「go to defition of…」 定位到了下面的定義:

#define IS_GPIO_ALL_INSTANCE(INSTANCE) (((INSTANCE) == GPIOA) || \

((INSTANCE) == GPIOB) || \

((INSTANCE) == GPIOC) || \

((INSTANCE) == GPIOD) || \

((INSTANCE) == GPIOE) || \

((INSTANCE) == GPIOF) || \

((INSTANCE) == GPIOG))

很明顯可以看出,GPIOx 的取值規定只允許是 GPIOA~GPIOG。

同樣的辦法,我們雙擊「IS_GPIO_PIN」 右鍵點擊「go to defition of…」,定位到下面的定義:

#define IS_GPIO_PIN(PIN) (((((uint32_t)PIN) & GPIO_PIN_MASK ) != 0x00u)

&& ((((uint32_t)PIN) & ~GPIO_PIN_MASK) == 0x00u))

同時,宏定義標識符 GPIO_PIN_MASK 的定義為:

#define GPIO_PIN_MASK 0x0000FFFFu

從上面可以看出,PIN 取值只要低 16 位不為 0 即可。這裡需要大家注意,因為一組 IO 口只有

16 個 IO,實際上 PIN 的值在這裡只有低 16 位有效,所以 PIN 的取值範圍為 0x0001~0xFFFF。

那麼是不是我們寫代碼初始化就是直接給一個 16 位的數字呢?這也是可以的,但是大多數情況

下,我們不會直接在入口參數處設置一個簡單的數字,因為這樣代碼的可讀性太差,HAL 庫會

將這些數字的含義通過宏定義定義出來,這樣可讀性大大增強。我們可以看到在

GPIO_PIN_MASK 宏定義的上面還有數行宏定義:

#define GPIO_PIN_0 ((uint16_t)0x0001)

#define GPIO_PIN_1 ((uint16_t)0x0002)

#define GPIO_PIN_2 ((uint16_t)0x0004)

…//此處省略部分定義

#define GPIO_PIN_14 ((uint16_t)0x4000)

#define GPIO_PIN_15 ((uint16_t)0x8000)

#define GPIO_PIN_All ((uint16_t)0xFFFF)

這些宏定義 GPIO_PIN_0 ~ GPIO_PIN_All 就是 HAL 庫事先定義好的,我們寫代碼的時候初始

化結構體 成員變量 Pin 的時候入口參數可以是這些宏定義標識符。

同理,對於成員變量 Pull,我們用同樣的方法,可以找到其取值範圍定義為:

#define IS_GPIO_PULL(PULL) (((PULL) == GPIO_NOPULL)\

|| ((PULL) == GPIO_PULLUP) || \ ((PULL) == GPIO_PULLDOWN))

也就是 PULL 的 取 值 範 圍 只 能 是 標 識 符 GPIO_NOPULL , GPIO_PULLUP 以 及

GPIO_PULLDOWN。

對於成員變量 Mode,方法都是一樣的,這裡基於篇幅考慮我們就不重複講解。講到這裡,

我們基本對 HAL_GPIO_Init 的入口參數有比較詳細的了解了。於是我們可以組織起來下面的代

碼:

GPIO_InitTypeDef GPIO_Initure;

GPIO_Initure.Pin=GPIO_PIN_9; //PA9

GPIO_Initure.Mode=GPIO_MODE_AF_PP; //復用推輓輸出

GPIO_Initure.Pull=GPIO_PULLUP; //上拉

GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;//高速

HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9

接著又有一個問題會被提出來,這個初始化函數一次只能初始化一個 IO 口嗎?我要同時

初始化很多個 IO 口,是不是要複製很多次這樣的初始化代碼呢?

這裡又有一個小技巧了。從上面的 GPIO_PIN_X 的宏定義我們可以看出,這些值是 0,1,2,4

這樣的數字,所以每個 IO 口選定都是對應著一個位,16 位的數據一共對應 16 個 IO 口。這個

位為 0 那麼這個對應的 IO 口不選定,這個位為 1 對應的 IO 口選定。如果多個 IO 口,他們都

是對應同一個 GPIOx,那麼我們可以通過|(或)的方式同時初始化多個 IO 口。這樣操作的前

提是,他們的 Mode,Speed 和 Pull 參數值相同,因為這些參數並不能一次定義多種。所以初始

化多個具有相同配置的 IO 口的方式可以是如下:

GPIO_InitTypeDef GPIO_Initure;

GPIO_Initure.Pin=GPIO_PIN_9| GPIO_PIN_10| GPIO_PIN_11; //PA9,PA10,PA11

GPIO_Initure.Mode=GPIO_MODE_AF_PP; //復用推輓輸出

GPIO_Initure.Pull=GPIO_PULLUP; //上拉

GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;//高速

HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9 ,PA10,PA11

對於那些參數可以通過|(或)的方式連接,這既有章可循,同時也靠大家在開發過程中不斷積累。

大家會覺得上面講解有點麻煩,每次要去查找 assert_param()這個函數去尋找,那麼有沒有

更好的辦法呢?大家可以打開 GPIO_InitTypeDef 結構體定義:

typedef struct

{

uint32_t Pin; /*!< Specifies the GPIO pins to be configured.

This parameter can be any value of @ref GPIO_pins_define */

uint32_t Mode; /*!< Specifies the operating mode for the selected pins.

This parameter can be a value of @ref GPIO_mode_define */

uint32_t Pull; /*!< Specifies the Pull-up or Pull-Down activation for the selected pins.

This parameter can be a value of @ref GPIO_pull_define */

uint32_t Speed; /*!< Specifies the speed for the selected pins.

This parameter can be a value of @ref GPIO_speed_define */

}GPIO_InitTypeDef;

從上圖的結構體成員後面的注釋我們可以看出 Pin 的意思是

「Specifies the GPIO pins to be configured.

This parameter can be any value of @ref GPIO_pins_define」。

從這段注釋可以看出 Pin 的取值需要參考注釋 GPIO_pins_define,大家可以在 MDK 中搜索注釋

GPIO_pins_define,就可以找到上面我們提到的 Pin 的取值範圍宏定義。如果要確定詳細的信息

我們就得去查看手冊了。對於去查看手冊的哪個地方,你可以在函數 HAL_GPIO_Init ()的函數

體中搜索 Pin 關鍵字,然後查看庫函數設置 Pin 是設置的哪個寄存器的哪個位,然後去中文參

考手冊查看該寄存器相應位的定義以及前後文的描述。

這一節我們就講解到這裡,希望能對大家的開發有幫助。

相關焦點

  • 正點原子-戰艦V3第十章 外部中斷實驗
    這一章,我們將向大家介紹如何使用 STM32F1 的外部輸入中斷。在前面幾章的學習中,我們掌握了 STM32F1 的 IO 口最基本的操作。本章分為如下幾個部分:10.1 STM32F1 外部中斷簡介10.2 硬體設計10.3 軟體設計10.4 下載驗證10.1 STM32F1 外部中斷簡介STM32F1
  • 正點原子-戰艦V3第三十七章 MPU6050 六軸傳感器實驗
    ALIENTEK 戰艦 STM32F1 開發板本身並不帶 MPU6050 傳感器,但是可以通過 ATK MODULE接口,外擴 ATK-MPU6050 模塊來實現本例程。個部分介紹:1,MPU6050 基礎介紹。
  • 正點原子-戰艦V3第三十六章 DHT11 數字溫溼度傳感器實驗
    本章分為如下幾個部分:36.1 DHT11 簡介36.2 硬體設計36.3 軟體設計36.4 下載驗證36.1 DHT11 簡介DHT11>通過以上了解,我們就可以通過 STM32F1 來實現對 DHT11 的讀取了。
  • STM32F1與STM32F0在GPIO_TypeDef 寄存器方面的不同
    一種相對簡單的使用HSI配置系統時鐘為64MHz的方法 最近因為項目成本需要,必須使用片內晶振作為系統的時鐘源,所以對正點原子的項目模板時鐘配置做了一些修改。  &nbsp 發表於 2020-02-24 淺談stm32f1兩路adc採集(非DMA) 由於項目的需要寫2路ADC採集,發現採集的電壓出現很大的誤差。
  • STM32CubeMX升級至V5.2.0,STM32G4即將推出,STM32CubeIDE正式面世
    官網網址: www.st.com/stm32cubemx 1.增加對STM32H7系列新雙核設備的支持 針對H7的內容,新增一些型號,和板卡的支持,大致如下: 官網網址: https://www.stmicroelectronics.com.cn/en/development-tools/stm32cubeide.html
  • 佳能RF 35mm f/1.8 Macro IS STM和佳能RF 85mm f/2 IS STM【參數...
    佳能RF 35mm f/1.8 Macro IS STM和佳能RF 85mm f/2 IS STM有什麼區別標準變焦鏡頭的確適合作為新手入門的第一支鏡頭,變焦可以加深新手對於焦段的理解,適用的拍攝題材也更加廣泛,可以創作出風光、人像、人文紀實等多樣的作品。佳能RF35mm F1.8 MACR...
  • 拆解正點原子DS100mini示波器
    拆解正點原子DS100mini示波器 免責聲明: 該內容由專欄作者授權發布或轉載自其他媒體,目的在於傳遞更多信息,並不代表本網贊同其觀點,本站亦不保證或承諾內容真實性等。
  • 如何實現STM32F407單片機的ADC轉換
    用到的引腳是PA3也就是ADC1的通道3 1、ADC的主要參數 a、解析度----stm32f407的解析度有6位、8位、10位、12位,參考電壓如果是3.3 那麼最小解析度就是3.3/4095。 b、轉換時間----stm32f407的最高允許頻率是36M,最快轉換時間 = 3+12個周期 =0.71us。
  • STM8 STM32 數據類型
    比如STM32F429就自帶了第四,你還需要了解的是編譯器,編譯器的數據類型一些定義,及有些編譯器的可能自帶內建函數,這個不必須了解。否則無法編程。本文主要就介紹STM32的數據類型的基礎知識。以及一些關鍵詞的意義。
  • STM8S單片機入門2(關機模式開關機按鈕)
    1)引腳工作模式設置首先要做的,就是設置單片機引腳的工作模式,把PD4設置為輸入模式,D3設置為輸出模式。4)外部中斷處理程序在ST官方提供的STM8S_StdPeriph_Lib庫自帶的模板中,中斷處理程序都在stm8s_it.c中實現。stm8s_it.c中已經根據中斷向量設置好了每種中斷處理程序的入口函數。只需在相應函數中填入內容即可。
  • 「高二輔導」高中物理選修3-5第十八章《原子結構》章末總結
    高中物理選修3-5第十八章《原子結構》在高考中基本上以選擇題的形式出現,主要涉及到基本知識的記憶與理解。其中最重點的要數第四節《氫原子光譜》。下面來梳理一下本章主要知識:首先列一個提綱1、電子的發現:(1)湯姆孫發現電子(2)湯姆孫提出「棗糕模型」2、原子核式結構模型
  • stm32f103c8t6封裝及最小系統原理圖
    打開APP stm32f103c8t6封裝及最小系統原理圖 發表於 2017-11-23 15:50:30   STM32F103C8T6是一款集成電路,芯體尺寸為32位,程序存儲器容量是64KB,需要電壓2V~3.6V,工作溫度為-40°C ~ 85°C。
  • 佳能RF50 F1.8 RF70-200 F4 12月上市
    佳能正式發布了rf50mmf1.8stm和rf70-200mmf4lisusm鏡片,將分別於12月初和12月中旬上市。Rf50mmf1.8stm使用五組六種光學結構,包括一個最大孔徑為f1.8的非球面透鏡和最小孔徑為F22的非球面透鏡。利用STM步進聚焦電機,最近的焦距為0.3m,放大倍數高達0.25x。
  • 課件:高中化學選修四原電池內容(第四章第1節)
    下列是選修四第四章第一節原電池的課件內容(圖片),包含重點知識和對應例題練習,總共包含三個部分。如果需要查看課件可至百度文庫(https://wenku.baidu.com/view/69bc7e2cf32d2af90242a8956bec0975f465a434)本節主要包含3個部分知識點:一是對原電池(必修二)內容的複習
  • 306頁現場電工全能圖解,電工人由入門到高手,常用知識精選合集
    306頁現場電工全能圖解,電工人由入門到高手,常用知識精選合集!本套資料以框圖和表格為主要敘述形式,詳細闡述了建築工程施工現場電工的工作職責及專業技術知識。全資料共分十三章,主要內容包括現場電工常用資料及設備、現場電工識圖基礎知識、施工現場配電線路、施工現場配電裝置、電力變壓器、柴油發電機組、電動機、母線加工與安裝施工現場電氣照明裝置、施工現場保護接地與防雷、施工現場用電防火與防爆、施工現場臨時用電安全管理及觸電事故與急救等。
  • 2011年統計從業考試《統計基礎知識》第五章至第八章 考試大綱
    發表時間:2011/3/15 15:27:06 來源:網際網路點擊關注微信:《統計基礎知識》第五至第八章  考試大綱第五章  時間數列學習目的與要求:學習並掌握時間數列的概念、作用、種類、特徵和編制原則;計算平均發展水平(序時平均數)
  • 秀操作:用HC-SR04和STM32F103ZET6做超聲波測距
    2.要掌握的知識點和設備我這邊用的是HC-SR04模塊+STM32F103ZET6開發板+示波器,示波器是幫助分析用,可以驗證設計和實際是否一致的工具,可以不要。2.2軟體知識要用上面這套工具實現超聲波測距的功能,需要的代碼知識點也說過了,這裡再提一下。a.PWM輸出一個脈衝大於10us的方波到Trig,可以用STM32的定時器輸出b.輸入捕獲Echo接受到的高電平信號,通過測量接受到的高電平時間,即可通過距離=速度*時間/2計算出距離。c.串口調試,我們要通過串口調試助手列印出測量的時間和距離,可以方便直觀的看到我們的結果。
  • 中考物理順口溜學習之第三章和第四章筆記匯總
    中考必考考點之物理順口溜匯總,教你輕鬆學物理本次課程我們接著上次課程的內容,來為大家分享一下初二物理第三章和第四章的考點內容,編輯成順口溜,教你輕鬆理解和記憶考點,在中考中拿下好成績。第三章----透鏡及其應用1.透鏡說透鏡,能透光;中間厚,凸透鏡;中間薄,凹透鏡。會聚作用凸透鏡,發散作用凹透鏡。
  • I2C基礎知識入門簡單介紹
    I2C基礎知識入門簡單介紹 玩轉單片機 發表於 2020-12-02 14:29:05 廢話 I2C其實肝的我挺難受的,通訊協議這種規範往往可以摳出很多的細節
  • 黃老師聊數學(214)微積分入門的一些基礎知識
    微分學。第一次知道球的體積和表面積的關係,圓的面積和周長之間的關係的時候,是多麼令人驚嘆!極限求面積,累加求和有技巧。積分的基本思想,極限為工具。首先得知道下面函數f(x)積分過程的表示,一整套符號的含義。這些微分積分符號都是萊布尼茲的發明,他不愧是一位符號大師。數學本身也是符號的語言,如何簡潔地用符號表示數學的原理和思想是很重要的。