STM32F10xx 參考手冊

6.?什么是寄存器

本章參考資料:《STM32F10xx 參考手冊》、《STM32F10xx數據手冊》、

學習本章時,配合《STM32F10xx 參考手冊》“存儲器和總線架構”及“通用I/O(GPIO)”章節一起閱讀,效果會更佳,特別是涉及到寄存器說明的部分。

6.1.?什么是寄存器

我們經常說寄存器,那么什么是寄存器?這是我們本章需要講解的內容,在學習的過程中,大家帶著這個疑問好好思考下,到最后看看大家能否用一句話給寄存器下一個定義。

6.2.?STM32長啥樣

我們開發板中使用的芯片是64pin的STM32F103RCT6,具體見圖?STM32F103RCT6實物圖。 這個就是我們接下來要學習的STM32,它將帶領我們進入嵌入式的殿堂。

STM32F103RCT6實物圖(紅色框中部分)

芯片正面是絲印,ARM應該是表示該芯片使用的是ARM的內核,STM32F103RCT6是芯片型號,后面的字應該是跟生產批次相關,最上面的是ST的LOGO。

芯片四周是引腳,左下角的小圓點表示1腳,然后從1腳起按照逆時針的順序排列(所有芯片的引腳順序都是逆時針排列的)。 開發板中把芯片的引腳引出來,連接到各種傳感器上,然后在STM32上編程(實際就是通過程序控制這些引腳輸出高電平或者低電平)來控制各種傳感器工作, 通過做實驗的方式來學習STM32芯片的各個資源。開發板是一種評估板,板載資源非常豐富,引腳復用比較多, 力求在一個板子上驗證芯片的全部功能。STM32F103RCT6正面引腳圖

STM32F103RCT6正面引腳圖

6.3.?芯片里面有什么

我們看到的STM32芯片是已經封裝好的成品,主要由內核和片上外設組成。若與電腦類比,內核與外設就如同電腦上的CPU與主板、內存、顯卡、硬盤的關系。

STM32F103采用的是Cortex-M3內核,內核即CPU,由ARM公司設計。ARM公司并不生產芯片,而是出售其芯片技術授權。 芯片生產廠商(SOC)如ST、TI、Freescale,負責在內核之外設計部件并生產整個芯片,這些內核之外的部件被稱為核外外設或片上外設。 如GPIO、USART(串口)、I2C、SPI等都叫做片上外設。具體見圖?STM32芯片架構簡圖。

STM32芯片架構簡圖

芯片(這里指內核,或者叫CPU)和外設之間通過各種總線連接,其中驅動單元有4個,被動單元也有4個, 具體見圖?STM32F10xx系統框圖。為了方便理解,我們都可以把驅動單元理解成是CPU部分,被動單元都理解成外設。 下面我們簡單介紹下驅動單元和被動單元的各個部件。

6.3.1.?ICode總線

ICode中的I表示Instruction,即指令。我們寫好的程序編譯之后都是一條條指令,存放在FLASH中, 內核要讀取這些指令來執行程序就必須通過ICode總線,它幾乎每時每刻都需要被使用,它是專門用來取指的。

6.3.2.?驅動單元

6.3.2.1.?DCode總線

DCode中的D表示Data,即數據,那說明這條總線是用來取數的。我們在寫程序的時候,數據有常量和變量兩種, 常量就是固定不變的,用C語言中的const關鍵字修飾,是放到內部的FLASH當中的,變量是可變的,不管是全局變量還是局部變量都放在內部的SRAM。 因為數據可以被Dcode總線和DMA總線訪問,所以為了避免訪問沖突,在取數的時候需要經過一個總線矩陣來仲裁,決定哪個總線在取數。

6.3.2.2.?系統總線

系統總線主要是訪問外設的寄存器,我們通常說的寄存器編程,即讀寫寄存器都是通過這根系統總線來完成的。

6.3.2.3.?DMA總線

DMA總線也主要是用來傳輸數據,這個數據可以是在某個外設的數據寄存器,可以在SRAM,可以在內部的FLASH。 因為數據可以被Dcode總線和DMA總線訪問,所以為了避免訪問沖突,在取數的時候需要經過一個總線矩陣來仲裁,決定哪個總線在取數。

6.3.3.?被動單元

6.3.3.1.?內部的閃存存儲器

內部的閃存存儲器即FLASH,我們編寫好的程序就放在這個地方。內核通過ICode總線來取里面的指令。

6.3.3.2.?內部的SRAM

內部的SRAM,即我們通常說的RAM,程序的變量,堆棧等的開銷都是基于內部的SRAM。內核通過DCode總線來訪問它。

6.3.3.3.?FSMC

FSMC的英文全稱是Flexible static memory controller,叫靈活的靜態的存儲器控制器, 是STM32F10xx中一個很有特色的外設, 通過FSMC,我們可以擴展內存,如外部的SRAM,NANDFLASH和NORFLASH。但有一點我們要注意的是,FSMC只能擴展靜態的內存, 即名稱里面的S:static,不能是動態的內存,比如SDRAM就不能擴展。

6.3.3.4.?AHB到APB的橋

從AHB總線延伸出來的兩條APB2和APB1總線,上面掛載著STM32各種各樣的特色外設。我們經常說的GPIO、串口、I2C、SPI這些外設就掛載在這兩條總線上, 這個是我們學習STM32的重點,就是要學會編程這些外設去驅動外部的各種設備。

STM32F10xx系統框圖(不包括互聯型)

6.4.?存儲器映射

在圖?STM32F10xx系統框圖?中,被控單元的FLASH, RAM,FSMC和AHB到APB的橋(即片上外設),這些功能部件共同排列在一個4GB的地址空間內。 我們在編程的時候,可以通過他們的地址找到他們,然后來操作他們(通過C語言對它們進行數據的讀和寫)。

6.4.1.?存儲器映射

存儲器本身不具有地址信息,它的地址是由芯片廠商或用戶分配,給存儲器分配地址的過程就稱為存儲器映射, 具體見圖?存儲器映射。如果給存儲器再分配一個地址就叫存儲器重映射。

存儲器映射(摘自參考手冊-存儲器映射章節)

6.4.1.1.?存儲器區域功能劃分

在這4GB的地址空間中,ARM已經粗線條的平均分成了8個塊,每塊512MB,每個塊也都規定了用途,具體分類見表格?存儲器功能分類。 每個塊的大小都有512MB,顯然這是非常大的,芯片廠商在每個塊的范圍內設計各具特色的外設時并不一定都用得完,都是只用了其中的一部分而已。

存儲器功能分類

在這8個Block里面,有3個塊非常重要,也是我們最關心的三個塊。Block0用來設計成內部FLASH,Block1用來設計成內部RAM, Block2用來設計成片上的外設,下面我們簡單的介紹下這三個Block里面的具體區域的功能劃分。

6.4.1.1.1.?存儲器Block0內部區域功能劃分

Block0主要用于設計片內的FLASH,我們使用的STM32F103ZET6(霸道)和STM32F103VET6(指南者)的FLASH都是512KB, 屬于大容量。要在芯片內部集成更大的FLASH或者SRAM都意味著芯片成本的增加,往往片內集成的FLASH都不會太大, ST能在追求性價比的同時做到512KB,實乃良心之舉。Block內部區域的功能劃分具體見表格?存儲器Block0內部區域功能劃分。

存儲器Block0內部區域功能劃分

6.4.1.1.2.?儲存器Block1內部區域功能劃分

Block1用于設計片內的SRAM。我們使用的STM32F103ZET6(霸道)和STM32F103VET6(指南者)的SRAM都是64KB, Block內部區域的功能劃分具體見表格?存儲器Block1內部區域功能劃分。

存儲器Block1內部區域功能劃分

6.4.1.1.3.?儲存器Block2內部區域功能劃分

Block2用于設計片內的外設,根據外設的總線速度不同,Block被分成了APB和AHB兩部分,其中APB又被分為APB1和APB2, 具體見表格?存儲器Block2內部區域功能劃分。

存儲器Block2內部區域功能劃分

6.5.?寄存器映射

我們知道,存儲器本身沒有地址,給存儲器分配地址的過程叫存儲器映射,那什么叫寄存器映射?寄存器到底是什么?

在存儲器Block2這塊區域,設計的是片上外設,它們以四個字節為一個單元,共32bit,每一個單元對應不同的功能, 當我們控制這些單元時就可以驅動外設工作。我們可以找到每個單元的起始地址,然后通過C語言指針的操作方式來訪問這些單元, 如果每次都是通過這種地址的方式來訪問,不僅不好記憶還容易出錯,這時我們可以根據每個單元功能的不同,以功能為名給這個內存單元取一個別名, 這個別名就是我們經常說的寄存器,這個給已經分配好地址的有特定功能的內存單元取別名的過程就叫寄存器映射。

比如,我們找到GPIOB端口的輸出數據寄存器ODR的地址是0x40010C0C(至于這個地址如何找到可以先跳過,后面我們會有詳細的講解), ODR寄存器是32bit,低16bit有效,對應著16個外部IO,寫0/1對應的的IO則輸出低/高電平。現在我們通過C語言指針的操作方式, 讓GPIOB的16個IO都輸出高電平,具體見代碼清單:寄存器-1。

代碼清單:寄存器-1 通過絕對地址訪問內存單元

1
2
// GPIOB 端口全部輸出 高電平
*(unsigned int*)(0x4001 0C0C) = 0xFFFF;

0x4001 0C0C在我們看來是GPIOB端口ODR的地址,但是在編譯器看來,這只是一個普通的變量,是一個立即數, 要想讓編譯器也認為是指針,我們得進行強制類型轉換,把它轉換成指針, 即(unsigned int *)0x4001 0C0C,然后再對這個指針進行 * 操作。

剛剛我們說了,通過絕對地址訪問內存單元不好記憶且容易出錯,我們可以通過寄存器的方式來操作,具體見代碼清單:寄存器-2。

代碼清單:寄存器-2 通過寄存器別名方式訪問內存單元

1
2
3
// GPIOB 端口全部輸出 高電平
#define GPIOB_ODR                   (unsigned int*)(GPIOB_BASE+0x0C)
* GPIOB_ODR = 0xFF;

為了方便操作,我們干脆把指針操作“*”也定義到寄存器別名里面,具體見代碼清單:寄存器-3。

代碼清單:寄存器-3 通過寄存器別名訪問內存單元

1
2
3
// GPIOB 端口全部輸出 高電平
#define GPIOB_ODR                   *(unsigned int*)(GPIOB_BASE+0x0C)
GPIOB_ODR = 0xFF;

6.5.1.?STM32的外設地址映射

片上外設區分為三條總線,根據外設速度的不同,不同總線掛載著不同的外設,APB1掛載低速外設,APB2和AHB掛載高速外設。 相應總線的最低地址我們稱為該總線的基地址,總線基地址也是掛載在該總線上的首個外設的地址。其中APB1總線的地址最低,片上外設從這里開始,也叫外設基地址。

6.5.1.1.?總線基地址

總線基地址

表格?總線基地址?的“相對外設基地址偏移”即該總線地址與“片上外設”基地址0x4000 0000的差值。關于地址的偏移我們后面還會講到。

6.5.1.2.?外設基地址

總線上掛載著各種外設,這些外設也有自己的地址范圍,特定外設的首個地址稱為“XX外設基地址”,也叫XX外設的邊界地址。 具體有關STM32F10xx外設的邊界地址請參考《STM32F10xx參考手冊》的2.3小節的存儲器映射的表1:STM32F10xx 寄存器邊界地址。

這里面我們以GPIO這個外設來講解外設的基地址,GPIO屬于高速的外設 ,掛載到APB2總線上,具體見表格?外設GPIO基地址。

外設GPIO基地址

6.5.1.3.?外設寄存器

在XX外設的地址范圍內,分布著的就是該外設的寄存器。以GPIO外設為例,GPIO是通用輸入輸出端口的簡稱, 簡單來說就是STM32可控制的引腳,基本功能是控制引腳輸出高電平或者低電平。最簡單的應用就是把GPIO的引腳連接到LED燈的陰極, LED燈的陽極接電源,然后通過STM32控制該引腳的電平,從而實現控制LED燈的亮滅。

GPIO有很多個寄存器,每一個都有特定的功能。每個寄存器為32bit,占四個字節,在該外設的基地址上按照順序排列, 寄存器的位置都以相對該外設基地址的偏移地址來描述。這里我們以GPIOB端口為例,來說明GPIO都有哪些寄存器, 具體見表格?GPIOB端口的寄存器地址列表。

GPIOB端口的寄存器地址列表

有關外設的寄存器說明可參考《STM32F10xx參考手冊》中具體章節的寄存器描述部分,在編程的時候我們需要反復的查閱外設的寄存器說明。

這里我們以“GPIO端口置位/復位寄存器”為例,教大家如何理解寄存器的說明, 具體見圖?GPIO端口置位_復位寄存器說明。

GPIO端口置位_復位寄存器說明

  • ①名稱

    寄存器說明中首先列出了該寄存器中的名稱,“(GPIOx_BSRR)(x=A…E)”這段的意思是該寄存器名為“GPIOx_BSRR”其中的“x”可以為A-E, 也就是說這個寄存器說明適用于GPIOA、GPIOB至GPIOE,這些GPIO端口都有這樣的一個寄存器。

  • ②偏移地址

    偏移地址是指本寄存器相對于這個外設的基地址的偏移。本寄存器的偏移地址是0x10, 從參考手冊中我們可以查到GPIOA外設的基地址為0x4001 0800 , 我們就可以算出GPIOA的這個GPIOA_BSRR寄存器的地址為:0x4001 0800+0x10;同理, 由于GPIOB的外設基地址為0x4001 0C00, 可算出GPIOB_BSRR寄存器的地址為:0x4001 0C00+0x10。其他GPIO端口以此類推即可。

  • ③寄存器位表

    緊接著的是本寄存器的位表,表中列出它的0-31位的名稱及權限。表上方的數字為位編號,中間為位名稱,最下方為讀寫權限,其中w表示只寫, r表示只讀,rw表示可讀寫。本寄存器中的位權限都是w,所以只能寫,如果讀本寄存器,是無法保證讀取到它真正內容的。而有的寄存器位只讀, 一般是用于表示STM32外設的某種工作狀態的,由STM32硬件自動更改,程序通過讀取那些寄存器位來判斷外設的工作狀態。

  • ④位功能說明

    位功能是寄存器說明中最重要的部分,它詳細介紹了寄存器每一個位的功能。例如本寄存器中有兩種寄存器位,分別為BRy及BSy, 其中的y數值可以是0-15,這里的0-15表示端口的引腳號,如BR0、BS0用于控制GPIOx的第0個引腳,若x表示GPIOA,那就是控制GPIOA的第0引腳, 而BR1、BS1就是控制GPIOA第1個引腳。

    其中BRy引腳的說明是“0:不會對相應的ODRx位執行任何操作;1:對相應ODRx位進行復位”。這里的“復位”是將該位設置為0的意思, 而“置位”表示將該位設置為1;說明中的ODRx是另一個寄存器的寄存器位,我們只需要知道ODRx位為1的時候,對應的引腳x輸出高電平, 為0的時候對應的引腳輸出低電平即可(感興趣的讀者可以查詢該寄存器GPIOx_ODR的說明了解)。所以,如果對BR0寫入“1”的話, 那么GPIOx的第0個引腳就會輸出“低電平”,但是對BR0寫入“0”的話,卻不會影響ODR0位,所以引腳電平不會改變。要想該引腳輸出“高電平”, 就需要對“BS0”位寫入“1”,寄存器位BSy與BRy是相反的操作。

6.5.2.?C語言對寄存器的封裝

以上所有的關于存儲器映射的內容,最終都是為大家更好地理解如何用C語言控制讀寫外設寄存器做準備,此處是本章的重點內容。

6.5.2.1.?封裝總線和外設基地址

在編程上為了方便理解和記憶,我們把總線基地址和外設基地址都以相應的宏定義起來,總線或者外設都以他們的名字作為宏名, 具體見?代碼清單:寄存器-4。

代碼清單:寄存器-4 總線和外設基址宏定義

 123456789
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* 外設基地址 */
#define PERIPH_BASE           ((unsigned int)0x40000000)/* 總線基地址 */
#define APB1PERIPH_BASE       PERIPH_BASE
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)
#define AHBPERIPH_BASE        (PERIPH_BASE + 0x00020000)/* GPIO外設基地址 */
#define GPIOA_BASE            (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE            (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE            (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE            (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE            (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE            (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE            (APB2PERIPH_BASE + 0x2000)/* 寄存器基地址,以GPIOB為例 */
#define GPIOB_CRL             (GPIOB_BASE+0x00)
#define GPIOB_CRH             (GPIOB_BASE+0x04)
#define GPIOB_IDR             (GPIOB_BASE+0x08)
#define GPIOB_ODR             (GPIOB_BASE+0x0C)
#define GPIOB_BSRR            (GPIOB_BASE+0x10)
#define GPIOB_BRR             (GPIOB_BASE+0x14)
#define GPIOB_LCKR            (GPIOB_BASE+0x18)

代碼清單:寄存器-4?首先定義了 “片上外設”基地址PERIPH_BASE,接著在PERIPH_BASE上加入各個總線的地址偏移, 得到APB1、APB2總線的地址APB1PERIPH_BASE、APB2PERIPH_BASE,在其之上加入外設地址的偏移,得到GPIOA-G的外設地址, 最后在外設地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具體地址,就可以用指針讀寫, 具體見?代碼清單:寄存器-5。

代碼清單:寄存器-5 使用指針控制BSRR寄存器

1
2
3
4
5
6
7
8
9
/* 控制GPIOB 引腳0輸出低電平(BSRR寄存器的BR0置1) */
*(unsigned int *)GPIOB_BSRR = (0x01<<(16+0));/* 控制GPIOB 引腳0輸出高電平(BSRR寄存器的BS0置1) */
*(unsigned int *)GPIOB_BSRR = 0x01<<0;unsigned int temp;
/* 讀取GPIOB 端口所有引腳的電平(讀IDR寄存器) */
temp = *(unsigned int *)GPIOB_IDR;

該代碼使用 (unsigned int *) 把GPIOB_BSRR宏的數值強制轉換成了地址,然后再用“*”號做取指針操作,對該地址的賦值, 從而實現了寫寄存器的功能。同樣,讀寄存器也是用取指針操作,把寄存器中的數據取到變量里,從而獲取STM32外設的狀態。

6.5.2.2.?封裝寄存器列表

用上面的方法去定義地址,還是稍顯繁瑣,例如GPIOA-GPIOE都各有一組功能相同的寄存器,如GPIOA_ODR/GPIOB_ODR/GPIOC_ODR等等, 它們只是地址不一樣,但卻要為每個寄存器都定義它的地址。為了更方便地訪問寄存器,我們引入C語言中的結構體語法對寄存器進行封裝, 具體見?代碼清單:寄存器-6。

代碼清單:寄存器-6 使用結構體對GPIO寄存器組的封裝

 123456789
10
11
12
13
typedef unsigned           int uint32_t; /*無符號32位變量*/
typedef unsigned short     int uint16_t; /*無符號16位變量*//* GPIO寄存器列表 */
typedef struct {uint32_t CRL;     /*GPIO端口配置低寄存器    地址偏移: 0x00 */uint32_t CRH;     /*GPIO端口配置高寄存器    地址偏移: 0x04 */uint32_t IDR;     /*GPIO數據輸入寄存器      地址偏移: 0x08 */uint32_t ODR;     /*GPIO數據輸出寄存器      地址偏移: 0x0C */uint32_t BSRR;    /*GPIO位設置/清除寄存器   地址偏移: 0x10 */uint32_t BRR;     /*GPIO端口位清除寄存器     地址偏移: 0x14 */uint16_t LCKR;    /*GPIO端口配置鎖定寄存器   地址偏移: 0x18 */
} GPIO_TypeDef;

這段代碼用typedef 關鍵字聲明了名為GPIO_TypeDef的結構體類型,結構體內有7個 成員變量,變量名正好對應寄存器的名字。 C語言的語法規定,結構體內變量的存儲空間是連續的,其中32位的變量占用4個字節,16位的變量占用2個字節, 具體見圖?GPIO_TypeDef結構體成員的地址偏移。

GPIO_TypeDef結構體成員的地址偏移

也就是說,我們定義的這個GPIO_TypeDef ,假如這個結構體的首地址為0x4001 0C00(這也是第一個成員變量CRL的地址), 那么結構體中第二個成員變量CRH的地址即為0x4001 0C00 +0x04 ,加上的這個0x04,正是代表CRL所占用的4個字節地址的偏移量, 其它成員變量相對于結構體首地址的偏移,在上述代碼右側注釋已給。

這樣的地址偏移與STM32 GPIO外設定義的寄存器地址偏移一一對應,只要給結構體設置好首地址,就能把結構體內成員的地址確定下來, 然后就能以結構體的形式訪問寄存器,具體見?代碼清單:寄存器-7。

代碼清單:寄存器-7 通過結構體指針訪問寄存器

1
2
3
4
5
6
7
8
GPIO_TypeDef * GPIOx;      //定義一個GPIO_TypeDef型結構體指針GPIOx
GPIOx = GPIOB_BASE;        //把指針地址設置為宏GPIOB_BASE地址
GPIOx->IDR = 0xFFFF;
GPIOx->ODR = 0xFFFF;uint32_t temp;
temp = GPIOx->IDR;          //讀取GPIOB_IDR寄存器的值到變量temp中

這段代碼先用GPIO_TypeDef類型定義一個結構體指針GPIOx,并讓指針指向地址GPIOB_BASE(0x4001 0C00),使用地址確定下來, 然后根據C語言訪問結構體的語法,用GPIOx->ODR及GPIOx->IDR等方式讀寫寄存器。

最后,我們更進一步,直接使用宏定義好GPIO_TypeDef類型的指針,而且指針指向各個GPIO端口的首地址, 使用時我們直接用該宏訪問寄存器即可,具體?代碼清單:寄存器-8。

代碼清單:寄存器-8 定義好GPIO端口首地址址針

 123456789
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*使用GPIO_TypeDef把地址強制轉換成指針*/
#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE               ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF               ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH               ((GPIO_TypeDef *) GPIOH_BASE)/*使用定義好的宏直接訪問*/
/*訪問GPIOB端口的寄存器*/
GPIOB->BSRR = 0xFFFF;       //通過指針訪問并修改GPIOB_BSRR寄存器
GPIOB->CRL = 0xFFFF;        //修改GPIOB_CRL寄存器
GPIOB->ODR =0xFFFF;         //修改GPIOB_ODR寄存器uint32_t temp;
temp = GPIOB->IDR;          //讀取GPIOB_IDR寄存器的值到變量temp中/*訪問GPIOA端口的寄存器*/
GPIOA->BSRR = 0xFFFF;
GPIOA->CRL = 0xFFFF;
GPIOA->ODR =0xFFFF;uint32_t temp;
temp = GPIOA->IDR;          //讀取GPIOA_IDR寄存器的值到變量temp中

這里我們僅是以GPIO這個外設為例,給大家講解了C語言對寄存器的封裝。以此類推,其他外設也同樣可以用這種方法來封裝。好消息是, 這部分工作都由固件庫幫我們完成了,這里我們只是分析了下這個封裝的過程,讓大家知其然,也只其所以然。

6.5.3.?修改寄存器的位操作方法

使用C語言對寄存器賦值時,我們常常要求只修改該寄存器的某幾位的值,且其它的寄存器位不變,這個時候我們就需要用到C語言的位操作方法了。

6.5.3.1.?把變量的某位清零

此處我們以變量a代表寄存器,并假設寄存器中本來已有數值,此時我們需要把變量a的某一位清零,且其它位不變, 方法見?代碼清單:寄存器-9。

代碼清單:寄存器-9 對某位清零

 123456789
10
11
12
13
//定義一個變量a = 1001 1111 b (二進制數)
unsigned char a = 0x9f;//對bit2 清零a &= ~(1<<2);//括號中的1左移兩位,(1<<2)得二進制數:0000 0100 b
//按位取反,~(1<<2)得1111 1011 b
//假如a中原來的值為二進制數: a = 1001 1111 b
//所得的數與a作”位與&”運算,a = (1001 1111 b)&(1111 1011 b),
//經過運算后,a的值 a=1001 1011 b
// a的bit2 位被清零,而其它位不變。
6.5.3.2.?把變量的某幾個連續位清零

由于寄存器中有時會有連續幾個寄存器位用于控制某個功能,現假設我們需要把寄存器的某幾個連續位清零, 且其它位不變,方法見?代碼清單:寄存器-10。

代碼清單:寄存器-10 對某幾個連續位清零

 123456789
10
11
12
13
14
15
16
17
18
19
20
//若把a中的二進制位分成2個一組
//即bit0、bit1為第0組,bit2、bit3為第1組,
//  bit4、bit5為第2組,bit6、bit7為第3組
//要對第1組的bit2、bit3清零a &= ~(3<<2*1);//括號中的3左移兩位,(3<<2*1)得二進制數:0000 1100 b
//按位取反,~(3<<2*1)得1111 0011 b
//假如a中原來的值為二進制數: a = 1001 1111 b
//所得的數與a作”位與&”運算,a = (1001 1111 b)&(1111 0011 b),
//經過運算后,a的值 a=1001 0011 b
// a的第1組的bit2、bit3被清零,而其它位不變。//上述(~(3<<2*1))中的(1)即為組編號;如清零第3組bit6、bit7此處應為3
//括號中的(2)為每組的位數,每組有2個二進制位;若分成4個一組,此處即為4
//括號中的(3)是組內所有位都為1時的值;若分成4個一組,此處即為二進制數“1111 b”//例如對第2組bit4、bit5清零
a &= ~(3<<2*2);
6.5.3.3.?對變量的某幾位進行賦值。

寄存器位經過上面的清零操作后,接下來就可以方便地對某幾位寫入所需要的數值了,且其它位不變, 方法見?代碼清單:寄存器-11?,這時候寫入的數值一般就是需要設置寄存器的位參數。

代碼清單:寄存器-11 對某幾位進行賦值

1
2
3
4
5
//a = 1000 0011 b
//此時對清零后的第2組bit4、bit5設置成二進制數“01 b ”a |= (1<<2*2);
//a = 1001 0011 b,成功設置了第2組的值,其它組不變
6.5.3.4.?對變量的某位取反

某些情況下,我們需要對寄存器的某個位進行取反操作,即 1變0 ,0變1,這可以直接用如下操作,其它位不變, 見?代碼清單:寄存器-12。

代碼清單:寄存器-12 對某位進行取反操作

1
2
3
4
5
//a = 1001 0011 b
//把bit6取反,其它位不變a ^=(1<<6);
//a = 1101 0011 b

關于修改寄存器位的這些操作,在下一章中有應用實例代碼,可配合閱讀。

8.1.?GPIO簡介

GPIO是通用輸入輸出端口的簡稱,簡單來說就是STM32可控制的引腳,STM32芯片的GPIO引腳與外部設備連接起來,從而實現與外部通訊、 控制以及數據采集的功能。STM32芯片的GPIO被分成很多組,每組有16個引腳,如型號為STM32F103VET6型號的芯片有GPIOA、GPIOB、 GPIOC至GPIOE共5組GPIO,芯片一共100個引腳,其中GPIO就占了一大部分,所有的GPIO引腳都有基本的輸入輸出功能。

最基本的輸出功能是由STM32控制引腳輸出高、低電平,實現開關控制,如把GPIO引腳接入到LED燈,那就可以控制LED燈的亮滅, 引腳接入到繼電器或三極管,那就可以通過繼電器或三極管控制外部大功率電路的通斷。

最基本的輸入功能是檢測外部輸入電平,如把GPIO引腳連接到按鍵,通過電平高低區分按鍵是否被按下。

8.2.?GPIO框圖剖析

GPIO結構框圖

通過GPIO硬件結構框圖,就可以從整體上深入了解GPIO外設及它的各種應用模式。該圖從最右端看起,最右端就是代表STM32芯片引出的GPIO引腳,其余部件都位于芯片內部。

8.2.1.?基本結構分析

下面我們按圖中的編號對GPIO端口的結構部件進行說明。

8.2.1.1.?保護二極管及上、下拉電阻

引腳的兩個保護二級管可以防止引腳外部過高或過低的電壓輸入,當引腳電壓高于VDD時, 上方的二極管導通,當引腳電壓低于VSS時,下方的二極管導通,防止不正常電壓引入芯片導致芯片燒毀。 盡管有這樣的保護,并不意味著STM32的引腳能直接外接大功率驅動器件,如直接驅動電機,強制驅動要么電機不轉,要么導致芯片燒壞,必須要加大功率及隔離電路驅動。

8.2.1.2.?P-MOS管和N-MOS管

GPIO引腳線路經過兩個保護二極管后,向上流向“輸入模式”結構,向下流向“輸出模式”結構。先看輸出模式部分,線路經過一個由P-MOS和N-MOS管組成的單元電路。 這個結構使GPIO具有了“推挽輸出”和“開漏輸出”兩種模式。

所謂的推挽輸出模式,是根據這兩個MOS管的工作方式來命名的。在該結構中輸入高電平時,經過反向后,上方的P-MOS導通,下方的N-MOS關閉, 對外輸出高電平;而在該結構中輸入低電平時,經過反向后,N-MOS管導通,P-MOS關閉,對外輸出低電平。當引腳高低電平切換時,兩個管子輪流導通, P管負責灌電流,N管負責拉電流,使其負載能力和開關速度都比普通的方式有很大的提高。推挽輸出的低電平為0伏,高電平為3.3伏, 具體參考圖?推挽等效電路?,它是推挽輸出模式時的等效電路。

推挽等效電路

而在開漏輸出模式時,上方的P-MOS管完全不工作。如果我們控制輸出為0,低電平,則P-MOS管關閉,N-MOS管導通,使輸出接地, 若控制輸出為1 (它無法直接輸出高電平)時,則P-MOS管和N-MOS管都關閉,所以引腳既不輸出高電平,也不輸出低電平,為高阻態。 為正常使用時必須外部接上拉電阻,參考圖?開漏電路?中等效電路。它具有“線與”特性,也就是說,若有很多個開漏模式引腳連接到一起時, 只有當所有引腳都輸出高阻態,才由上拉電阻提供高電平,此高電平的電壓為外部上拉電阻所接的電源的電壓。若其中一個引腳為低電平, 那線路就相當于短路接地,使得整條線路都為低電平,0伏。

開漏電路

推挽輸出模式一般應用在輸出電平為0和3.3伏而且需要高速切換開關狀態的場合。在STM32的應用中,除了必須用開漏模式的場合,我們都習慣使用推挽輸出模式。

開漏輸出一般應用在I2C、SMBUS通訊等需要“線與”功能的總線電路中。除此之外,還用在電平不匹配的場合,如需要輸出5伏的高電平, 就可以在外部接一個上拉電阻,上拉電源為5伏,并且把GPIO設置為開漏模式,當輸出高阻態時,由上拉電阻和電源向外輸出5伏的電平, 具體見圖?STM32_IO對外輸出5V電平。

STM32_IO對外輸出5V電平

8.2.1.3.?輸出數據寄存器

前面提到的雙MOS管結構電路的輸入信號,是由GPIO“輸出數據寄存器GPIOx_ODR”提供的,因此我們通過修改輸出數據寄存器的值就可以修改GPIO引腳的輸出電平。 而“置位/復位寄存器GPIOx_BSRR”可以通過修改輸出數據寄存器的值從而影響電路的輸出。

8.2.1.4.?復用功能輸出

“復用功能輸出”中的“復用”是指STM32的其它片上外設對GPIO引腳進行控制,此時GPIO引腳用作該外設功能的一部分,算是第二用途。 從其它外設引出來的“復用功能輸出信號”與GPIO本身的數據據寄存器都連接到雙MOS管結構的輸入中,通過圖中的梯形結構作為開關切換選擇。

例如我們使用USART串口通訊時,需要用到某個GPIO引腳作為通訊發送引腳,這個時候就可以把該GPIO引腳配置成USART串口復用功能,由串口外設控制該引腳,發送數據。

// GPIOB 16個IO全部輸出 0XFFFF
GPIOB->ODR = 0XFFFF;
8.2.1.5.?輸入數據寄存器

看GPIO結構框圖的上半部分,GPIO引腳經過內部的上、下拉電阻,可以配置成上/下拉輸入,然后再連接到施密特觸發器,信號經過觸發器后, 模擬信號轉化為0、1的數字信號,然后存儲在“輸入數據寄存器GPIOx_IDR”中,通過讀取該寄存器就可以了解GPIO引腳的電平狀態。

// 讀取GPIOB端口的16位數據值
uint16_t temp;
temp = GPIOB->IDR;
8.2.1.6.?復用功能輸入

與“復用功能輸出”模式類似,在“復用功能輸入模式”時,GPIO引腳的信號傳輸到STM32其它片上外設,由該外設讀取引腳狀態。

同樣,如我們使用USART串口通訊時,需要用到某個GPIO引腳作為通訊接收引腳,這個時候就可以把該GPIO引腳配置成USART串口復用功能,使USART可以通過該通訊引腳的接收遠端數據。

8.2.1.7.?模擬輸入輸出

當GPIO引腳用于ADC采集電壓的輸入通道時,用作“模擬輸入”功能,此時信號是不經過施密特觸發器的,因為經過施密特觸發器后信號只有0、1兩種狀態, 所以ADC外設要采集到原始的模擬信號,信號源輸入必須在施密特觸發器之前。類似地,當GPIO引腳用于DAC作為模擬電壓輸出通道時,此時作為“模擬輸出”功能, DAC的模擬信號輸出就不經過雙MOS管結構,模擬信號直接輸出到引腳。

8.2.2.?GPIO工作模式

總結一下,由GPIO的結構決定了GPIO可以配置成以下模式:

代碼清單:點亮LED-1 GPIO 8種工作模式

 123456789
10
11
typedef enum
{GPIO_Mode_AIN = 0x0,           // 模擬輸入GPIO_Mode_IN_FLOATING = 0x04,  // 浮空輸入GPIO_Mode_IPD = 0x28,          // 下拉輸入GPIO_Mode_IPU = 0x48,          // 上拉輸入GPIO_Mode_Out_OD = 0x14,       // 開漏輸出GPIO_Mode_Out_PP = 0x10,       // 推挽輸出GPIO_Mode_AF_OD = 0x1C,        // 復用開漏輸出GPIO_Mode_AF_PP = 0x18         // 復用推挽輸出
} GPIOMode_TypeDef;

在固件庫中,GPIO總共有8種細分的工作模式,稍加整理可以大致歸類為以下三類:

8.2.2.1.?輸入模式(模擬/浮空/上拉/下拉)

在輸入模式時,施密特觸發器打開,輸出被禁止,可通過輸入數據寄存器GPIOx_IDR讀取I/O狀態。其中輸入模式,可設置為上拉、 下拉、浮空和模擬輸入四種。上拉和下拉輸入很好理解,默認的電平由上拉或者下拉決定。浮空輸入的電平是不確定的,完全由外部的輸入決定, 一般接按鍵的時候用的是這個模式。模擬輸入則用于ADC采集。

8.2.2.2.?輸出模式(推挽/開漏)

在輸出模式中,推挽模式時雙MOS管以輪流方式工作,輸出數據寄存器GPIOx_ODR可控制I/O輸出高低電平。開漏模式時,只有N-MOS管工作, 輸出數據寄存器可控制I/O輸出高阻態或低電平。輸出速度可配置,有2MHz10MHz50MHz的選項。此處的輸出速度即I/O支持的高低電平狀態最高切換頻率, 支持的頻率越高,功耗越大,如果功耗要求不嚴格,把速度設置成最大即可。

在輸出模式時施密特觸發器是打開的,即輸入可用,通過輸入數據寄存器GPIOx_IDR可讀取I/O的實際狀態。

8.2.2.3.?復用功能(推挽/開漏)

復用功能模式中,輸出使能,輸出速度可配置,可工作在開漏及推挽模式,但是輸出信號源于其它外設,輸出數據寄存器GPIOx_ODR無效; 輸入可用,通過輸入數據寄存器可獲取I/O實際狀態,但一般直接用外設的寄存器來獲取該數據信號。

通過對GPIO寄存器寫入不同的參數,就可以改變GPIO的工作模式,再強調一下, 要了解具體寄存器時一定要查閱《STM32F10X-中文參考手冊》中對應外設的寄存器說明。在GPIO外設中, 控制端口高低控制寄存器CRH和CRL可以配置每個GPIO的工作模式和工作的速度,每4個位控制一個IO, CRH控制端口的高八位,CRL控制端口的低8位,具體的看CRH和CRL的寄存器描述。

GPIO端口配置低寄存器

GPIO端口配置高寄存器

8.3.?實驗:使用寄存器點亮LED燈

本小節中,我們以實例講解如何通過控制寄存器來點亮LED燈。此處側重于講解原理,請直接用KEIL5軟件打開我們提供的實驗例程配合閱讀, 先了解原理,學習完本小節后,再嘗試自己建立一個同樣的工程。本節配套例程名稱為“GPIO輸出—使用寄存器點亮LED燈”, 在工程目錄下找到后綴為“.uvprojx”的文件,用KEIL5打開即可。

自己嘗試新建工程時,請對照查閱《新建工程— 寄存器版》章節。若沒有安裝KEIL5軟件,請參考《如何安裝KEIL5》章節。

打開該工程,見圖?工程文件結構?,可看到一共有三個文件,分別startup_stm32f10x_hd.s 、 stm32f10x.h 以及main.c,下面我們對這三個文件進行講解。

工程文件結構

8.3.1.?硬件連接

在本教程中STM32芯片與LED燈的連接見圖?LED燈電路連接圖?, 兩個LED燈的陽極引出連接到3.3V電源,陰極各經過1個限流電阻引入至STM32的2個GPIO引腳中, 所以我們只要控制這兩個引腳輸出高低電平, 即可控制其所連接LED燈的亮滅。如果您的實驗板STM32連接到LED燈的引腳或極性不一樣, 只需要修改程序到對應的GPIO引腳即可,工作原理都是一樣的。

我們的目標是把GPIO的引腳設置成推挽輸出模式并且默認下拉,輸出低電平,這樣就能讓LED燈亮起來了。

LED燈電路連接圖

8.3.2.?啟動文件

啟動文件在這里只是簡要的介紹下,關于這個文件的詳解請參考后面的《啟動文件詳解》章節。

名為“startup_stm32f10x_hd.s”的文件,它里邊使用匯編語言寫好了基本程序,當STM32芯片上電啟動的時候,首先會執行這里的匯編程序, 從而建立起C語言的運行環境,所以我們把這個文件稱為啟動文件。該文件使用的匯編指令是Cortex-M3內核支持的指令, 可參考《Cortex-M3權威指南》中指令集章節。

startup_stm32f10x_hd.s文件由官方提供,一般有需要也是在官方的基礎上修改,不會自己完全重寫。該文件從 ST 固件庫里面找到, 找到該文件后把啟動文件添加到工程里面即可。不同型號的芯片以及不同編譯環境下使用的匯編文件是不一樣的,但功能相同。

對于啟動文件這部分我們主要總結它的功能,不詳解講解里面的代碼,其功能如下:

  • 初始化堆棧指針SP;

  • 初始化程序計數器指針PC;

  • 設置堆、棧的大小;

  • 初始化中斷向量表;

  • 配置外部SRAM作為數據存儲器(這個由用戶配置,一般的開發板可沒有外部SRAM);

  • 調用SystemIni() 函數配置STM32的系統時鐘。

  • 設置C庫的分支入口“__main”(最終用來調用main函數);

先去除繁枝細節,挑重點的講,主要理解最后兩點,在啟動文件中有一段復位后立即執行的程序,代碼見?代碼清單:點亮LED-2。 在實際工程中閱讀時,可使用編輯器的搜索(Ctrl+F)功能查找這段代碼在文件中的位置,搜索Reset_Handler即可找到。

代碼清單:點亮LED-2復位后執行的程序

 123456789
10
11
;Reset handler
Reset_Handler    PROCEXPORT  Reset_Handler     [WEAK]IMPORT  SystemInitIMPORT  __mainLDR     R0, =SystemInitBLX     R0LDR     R0, =__mainBX      R0ENDP

開頭的是程序注釋,在匯編里面注釋用的是“;”,相當于 C 語言的“//”注釋符

第二行是定義了一個子程序:Reset_Handler。PROC 是子程序定義偽指令。這里就相當于C語言里定義了一個函數,函數名為Reset_Handler。

第三行 EXPORT 表示 Reset_Handler 這個子程序可供其他模塊調用。相當于C語言的函數聲明。關鍵字[WEAK] 表示弱定義, 如果編譯器發現在別處定義了同名的函數,則在鏈接時用別處的地址進行鏈接,如果其它地方沒有定義,編譯器也不報錯,以此處地址進行鏈接。

第四行和第五行 IMPORT 說明 SystemInit 和__main 這兩個標號在其他文件,在鏈接的時候需要到其他文件去尋找。 相當于C語言中,從其它文件引入函數聲明。以便下面對外部函數進行調用。

SystemInit 需要由我們自己實現,即我們要編寫一個具有該名稱的函數,用來初始化 STM32 芯片的時鐘,一般包括初始化AHB、APB等各總線的時鐘, 需要經過一系列的配置STM32才能達到穩定運行的狀態。其實這個函數在固件庫里面有提供,官方已經為我們寫好。

__main 其實不是我們定義的(不要與C語言中的main函數混淆),這是一個C庫函數,當編譯器編譯時,只要遇到這個標號就會定義這個函數, 該函數的主要功能是:負責初始化棧、堆,配置系統環境,并在函數的最后調用用戶編寫的 main 函數,從此來到 C 的世界。

第六行把 SystemInit 的地址加載到寄存器 R0。

第七行程序跳轉到 R0 中的地址執行程序,即執行SystemInit函數的內容。

第八行把__main 的地址加載到寄存器 R0。

第九行程序跳轉到 R0 中的地址執行程序,即執行__main函數,執行完畢之后就去到我們熟知的 C 世界,進入main函數。

第十行表示子程序的結束。

總之,看完這段代碼后,了解到如下內容即可:我們需要在外部定義一個SystemInit函數設置STM32的時鐘;STM32上電后,會執行SystemInit函數,最后執行我們C語言中的main函數。

8.3.3.?stm32f10x.h文件

看完啟動文件,那我們立即寫SystemInit和main函數吧?別著急,定義好了SystemInit函數和main我們又能寫什么內容?連接LED燈的GPIO引腳, 是要通過讀寫寄存器來控制的,就這樣空著手,如何控制寄存器。我們知道寄存器映射就是給一個已經分配好地址的特殊的內存空間取的一個別名, 這個特殊的內存空間就是寄存器,它可以通過指針來操作。在編程之前我們要先實現寄存器映射,有關寄存器映射的代碼都統一寫在stm32f10x.h文件中, 見?代碼清單:點亮LED-2。

代碼清單:點亮LED-3 外設地址定義

 123456789
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*片上外設基地址  */
#define PERIPH_BASE         ((unsigned int)0x40000000)/*總線基地址,GPIO都掛載到APB2上 */
#define APB2PERIPH_BASE     (PERIPH_BASE + 0x10000)
/* AHB總線基地址 */
#define AHBPERIPH_BASE      (PERIPH_BASE + 0x20000)/*GPIOB外設基地址*/
#define GPIOB_BASE          (APB2PERIPH_BASE + 0x0C00)/* GPIOB寄存器地址,強制轉換成指針 */
#define GPIOB_CRL           *(unsigned int*)(GPIOB_BASE+0x00)
#define GPIOB_CRH           *(unsigned int*)(GPIOB_BASE+0x04)
#define GPIOB_IDR           *(unsigned int*)(GPIOB_BASE+0x08)
#define GPIOB_ODR           *(unsigned int*)(GPIOB_BASE+0x0C)
#define GPIOB_BSRR          *(unsigned int*)(GPIOB_BASE+0x10)
#define GPIOB_BRR           *(unsigned int*)(GPIOB_BASE+0x14)
#define GPIOB_LCKR          *(unsigned int*)(GPIOB_BASE+0x18)/*RCC外設基地址*/
#define RCC_BASE           (AHBPERIPH_BASE + 0x1000)
/*RCC的AHB1時鐘使能寄存器地址,強制轉換成指針*/
#define RCC_APB2ENR        *(unsigned int*)(RCC_BASE+0x18)

GPIO外設的地址跟前面章節講解的相同,不過此處把寄存器的地址值都直接強制轉換成了指針,方便使用。 代碼的最后兩段是RCC外設寄存器的地址定義,RCC外設是用來設置時鐘的,以后我們會詳細分析,本實驗中只要了解到使用GPIO外設必須開啟它的時鐘即可。

8.3.4.?main文件

現在就可以開始編寫程序,在main文件中先編寫一個 main 函數,里面什么都沒有,暫時為空。

int main (void)
{
}

此時直接編譯的話,會出現如下錯誤:

Error: L6218E: Undefined symbol SystemInit (referred from startup_stm32f10x.o)

錯誤提示SystemInit 沒有定義。從分析啟動文件時我們知道,Reset_Handler 調用了該函數用來初始化SMT32系統時鐘, 為了簡單起見,我們在 main 文件里面定義一個 SystemInit空函數,什么也不做,為的是騙過編譯器,把這個錯誤去掉。 關于配置系統時鐘我們在后面再寫。當我們不配置系統時鐘時,STM32會把HSI當作系統時鐘,HSI=8M, 由芯片內部的振蕩器提供。我們在main中添加如下函數:

// 函數為空,目的是為了騙過編譯器不報錯
void SystemInit(void)
{
}

這時再編譯就沒有錯了,完美解決。還有一個方法就是在啟動文件中把有關SystemInit 的代碼注釋掉也可以,見?代碼清單:點亮LED-4。

代碼清單:點亮LED-4 注釋掉啟動文件中調用SystemInit的代碼

 123456789
10
11
; Reset handler
Reset_Handler    PROCEXPORT  Reset_Handler             [WEAK];IMPORT  SystemInitIMPORT  __main;LDR     R0, =SystemInit;BLX     R0LDR     R0, =__mainBX      R0ENDP

接下來在main函數中添加代碼,實現我們的點燈之旅。

8.3.4.1.?GPIO模式

首先我們把連接到LED燈的GPIO引腳配置成輸出模式,即配置GPIO的端口配置低寄存器CRL,見圖?GPIO端口控制低寄存器CRL。 CRL中包含0-7號引腳,每個引腳占用4個寄存器位。MODE位用來配置輸出的速度,CNF位用來配置各種輸入輸出模式。在這里我們把GPIO引腳配置為通用推挽輸出, 輸出的速度為10M,具體見?代碼清單:點亮LED-5。

代碼清單:點亮LED-5 配置輸出模式

1
2
3
4
// 清空控制PC2的端口位
GPIOC_CRL &= ~( 0x0F<< (4*2));
// 配置PC2為通用推挽輸出,速度為10M
GPIOC_CRL |= (1<<4*2);

GPIO端口控制低寄存器CRL

在代碼中,我們先把控制PC2的端口位清0,然后再向它賦值“0001 b”,從而使GPIOC2引腳設置成輸出模式,速度為10M。

代碼中使用了?&=~|=?這種操作方法是為了避免影響到寄存器中的其它位,因為寄存器不能按位讀寫,假如我們直接給CRL寄存器賦值:

GPIOC_CRL = 0x0000 0100;

這時CRL的的低8~11位被設置成“0001”輸出模式,但其它GPIO引腳就有意見了, 因為其它引腳的MODER位都已被設置成輸入模式(即全部被清0)。所以,為了在配置寄存器某個位的時候不影響其它位, 推薦使用“&=~”和“|=”這兩種操作方式,其中“&=~”用于清0,“|=”用于置1。

8.3.4.2.?控制引腳輸出電平

在輸出模式時,對端口位設置/清除寄存器BSRR寄存器、端口位清除寄存器BRR和ODR寄存器寫入參數即可控制引腳的電平狀態, 其中操作BSRR和BRR最終影響的都是ODR寄存器,然后再通過ODR寄存器的輸出來控制GPIO。為了一步到位,我們在這里直接操作ODR寄存器來控制GPIO的電平。 具體見?代碼清單:點亮LED-6。

代碼清單:點亮LED-6 控制引腳輸出電平

1
2
// PC2輸出低電平
GPIOC_ODR &= ~(1<<2);

數據輸出寄存器ODR

8.3.4.3.?開啟外設時鐘

設置完GPIO的引腳,控制電平輸出,以為現在總算可以點亮 LED 了吧,其實還差最后一步。由于STM32的 外設很多,為了降低功耗, 每個外設都對應著一個時鐘,在芯片剛上電的時候這些時鐘都是被關閉的,如果想要外設工作,必須把相應的時鐘打開。

STM32 的所有外設的時鐘由一個專門的外設來管理,叫 RCC(reset and clockcontrol), RCC 在《 STM32 中文參考手冊》的第六章。 關于RCC外設中的時鐘部分,我們在后面的章節《RCC—使用HSE/HIS配置》中有詳細的講解,這里我們暫時先了解下。

所有的 GPIO都掛載到 APB2 總線上,具體的時鐘由APB2外設時鐘使能寄存器(RCC_ APB2ENR)來控制, 具體見?代碼清單:點亮LED-7。

代碼清單:點亮LED-7 開啟端口時鐘

1
2
// 開啟 GPIOC 端口 時鐘
RCC_APB2ENR |= (1<<4);

APB2外設時鐘使能寄存器

8.3.4.4.?水到渠成

開啟時鐘,配置引腳模式,控制電平,經過這三步,我們總算可以控制一個 LED了。現在我們完整組織下用 STM32 控制一個 LED 的代碼, 見?代碼清單:點亮LED-8。

代碼清單:點亮LED-8 main文件中控制LED燈的代碼

 123456789
10
11
12
13
14
15
16
int main(void)
{// 開啟GPIOC 端口時鐘RCC_APB2ENR |= (1<<4);//清空控制PC2的端口位GPIOC_CRL &= ~( 0x0F<< (4*2));// 配置PC2為通用推挽輸出,速度為10MGPIOC_CRL |= (1<<4*2);// PC2 輸出 低電平GPIOC_ODR &= ~(1<<2);while (1);}

在本章節中,要求完全理解stm32f10x.h文件及main文件的內容(RCC相關的除外)。

8.3.5.?下載驗證

把編譯好的程序下載到開發板并復位,可看到板子上的LED燈被點亮。為了驗證是否掌握了本節課的知識點, 可以嘗試把另外一個LED也點亮,讓兩個燈一起實現流水燈的效果。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/905912.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/905912.shtml
英文地址,請注明出處:http://en.pswp.cn/news/905912.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

TCVectorDB 向量數據庫簡介

簡介 盡管目前大多數開源向量數據庫來自海外&#xff0c;配置簡單且性能優異&#xff0c;但由于網絡原因&#xff0c;如果向量數據庫部署在海外&#xff0c;而產品面向國內市場&#xff0c;網絡延遲將是必須考慮的問題。因此&#xff0c;選擇國內服務提供商的云向量數據庫往往是…

力扣-比特位計數(統計一個數二進制下1的個數)

下面是題面 1.用c的內置函數__builtin_popcount&#xff08;&#xff09; 語法&#xff1a;__builtin_popcount&#xff08;int x&#xff09;&#xff0c;函數會返回一個二進制下x所含的1的個數 2.直接數位枚舉 這是最慢也是暴力做法&#xff0c;寫法也很簡單 用一個while循環…

青少年編程與數學 02-019 Rust 編程基礎 16課題、包、單元包及模塊

青少年編程與數學 02-019 Rust 編程基礎 16課題、包、單元包及模塊 一、包1. **什么是 Crate&#xff1f;**2. **Crate 的類型**3. **Crate 的結構**4. **使用 Crate**5. **創建和管理 Crate**6. **發布 Crate**7. **Crate 的優勢**8. **示例**創建一個 library crate 二、單元…

強化學習入門:馬爾科夫獎勵過程二

文章目錄 前言1、動作2、策略總結 前言 最近想開一個關于強化學習專欄&#xff0c;因為DeepSeek-R1很火&#xff0c;但本人對于LLM連門都沒入。因此&#xff0c;只是記錄一些類似的讀書筆記&#xff0c;內容不深&#xff0c;大多數只是一些概念的東西&#xff0c;數學公式也不會…

【大數據知識】今天聊聊Clickhouse部署方案

ClickHouse部署 一、ClickHouse部署一、單節點部署1. 安裝準備2. 目錄規劃3. 核心配置4. 啟動服務 二、集群部署方案1. 集群拓撲設計2. 分布式配置3. 表引擎選擇 三、安全加固1. 認證配置2. SSL加密 四、性能優化1. 核心參數調優2. 資源隔離 五、監控與維護1. Prometheus 集成2…

打卡Day28

題目1&#xff1a;定義圓&#xff08;Circle&#xff09;類 要求&#xff1a; 1.包含屬性&#xff1a;半徑 radius。 2.包含方法&#xff1a; ●calculate_area()&#xff1a;計算圓的面積&#xff08;公式&#xff1a;πr&#xff09;。 ●calculate_circumference()&#xff…

BERT 進階:Albert 模型詳解與實戰

目錄 BERT 進階&#xff1a;Albert 模型詳解與實戰 一、ALBERT 的優化策略 &#xff08;一&#xff09;Embedding 參數因式分解 &#xff08;二&#xff09;跨層參數共享 &#xff08;三&#xff09;巨劍連貫性損失 二、ALBERT 模型架構 &#xff08;一&#xff09;Tran…

使用 163 郵箱實現 Spring Boot 郵箱驗證碼登錄

使用 163 郵箱實現 Spring Boot 郵箱驗證碼登錄 本文將詳細介紹如何使用網易 163 郵箱作為 SMTP 郵件服務器&#xff0c;實現 Spring Boot 項目中的郵件驗證碼發送功能&#xff0c;并解決常見配置報錯問題。 一、為什么需要郵箱授權碼&#xff1f; 出于安全考慮&#xff0c;大…

深入解析Spring Boot與Spring Security的集成實踐

深入解析Spring Boot與Spring Security的集成實踐 引言 在現代Web應用開發中&#xff0c;安全性是一個不可忽視的重要方面。Spring Security作為Spring生態中的安全框架&#xff0c;提供了強大的認證和授權功能。本文將結合Spring Boot&#xff0c;詳細介紹如何集成Spring Se…

C#將1GB大圖裁剪為8張圖片

C#處理超大圖片&#xff08;1GB&#xff09;需要特別注意內存管理和性能優化。以下是幾種高效裁剪方案&#xff1a; 方法1&#xff1a;使用System.Drawing分塊處理&#xff08;內存優化版&#xff09; using System; using System.Drawing; using System.Drawing.Imaging; us…

Linux系統啟動相關:vmlinux、vmlinuz、zImage,和initrd 、 initramfs,以及SystemV 和 SystemD

目錄 一、vmlinux、vmlinuz、zImage、bzImage、uImage 二、initrd 和 initramfs 1、initrd&#xff08;Initial RAM Disk&#xff09; 2、initramfs&#xff08;Initial RAM Filesystem&#xff09; 3、initrd vs. initramfs 對比 4. 如何查看和生成 initramfs 三、Syste…

AIStarter Windows 版本迎來重磅更新!模型插件工作流上線,支持 Ollama / ComfyUI 等多平臺本地部署模型統一管理

如果你正在使用 AIStarter 工具進行本地 AI 模型部署 &#xff0c;那么這條消息對你來說非常重要&#xff01; 在最新推出的 AIStarter Windows 正式版更新中 &#xff0c;官方對整個平臺進行了功能重構和性能優化&#xff0c;尤其是新增了「模型插件工作流 」功能&#xff0c…

深入理解橋接模式:解耦抽象與實現的設計藝術

一、為什么需要橋接模式&#xff1f;從“類爆炸”問題說起 你是否遇到過這樣的開發困境&#xff1f; 當需要為系統擴展新功能時&#xff0c;繼承體系像滾雪球一樣越變越臃腫&#xff1a;新增一種遙控器類型&#xff0c;需要為電視、音響各寫一個子類&#xff1b;新增一種設備類…

Java 中的泛型原理與實踐案例

引言&#xff1a;為什么需要泛型 在Java 5之前&#xff0c;集合類只能存儲Object類型的對象&#xff0c;這帶來了兩個主要問題&#xff1a; 類型不安全&#xff1a;可以向集合中添加任何類型的對象&#xff0c;容易出錯繁瑣的類型轉換&#xff1a;從集合中取出元素時需要手動…

springboot3+vue3融合項目實戰-大事件文章管理系統-獲取文章分類詳情

GetMapping("/detail")public Result<Category> detail(Integer id){Category c categoryService.findById(id);return Result.success(c);}在CategoryService接口增加 Category findById(Integer id); 在CategoryServiceImpl增加 Overridepublic Category f…

從零開始創建一個 Next.js 項目并實現一個 TodoList 示例

Next.js 是一個基于 React 的服務端渲染框架&#xff0c;它提供了很多開箱即用的功能&#xff0c;如自動路由、API 路由、靜態生成、增量靜態再生等。本文將帶你一步步創建一個 Next.js 項目&#xff0c;并實現一個簡單的 TodoList 功能。 效果地址 &#x1f9f1; 安裝 Next.j…

分布式鎖: Redisson紅鎖(RedLock)原理與實現細節

分布式鎖是分布式系統的核心基礎設施&#xff0c;但 單節點 Redis 鎖在高可用場景下存在致命缺陷&#xff1a;當 Redis 主節點宕機時&#xff0c;從節點可能因異步復制未完成而丟失鎖信息&#xff0c;導致多個客戶端同時持有鎖。為此&#xff0c;Redis 作者 Antirez 提出了 Red…

c++多態面試題之(析構函數與虛函數)

有以下問題展開 析構函數要不要定義成虛函數&#xff1f;基類的析構函數要不要定義成虛函數&#xff1f;如果不定義會有什么問題&#xff0c;定義了在什么場景下起作用。 1. 基類析構函數何時必須定義為虛函數&#xff1f; 當且僅當通過基類指針&#xff08;或引用&#xff09;…

Python高級進階:Vim與Vi使用指南

李升偉 整理 在 Python 高級進階中&#xff0c;使用 Vim 或 Vi 作為代碼編輯器可以顯著提升開發效率&#xff0c;尤其是在遠程服務器開發或快速腳本編輯時。以下是關于它們在 Python 開發中的高級應用詳解&#xff1a; 1. Vim/Vi 簡介 Vi&#xff1a;經典的 Unix 文本編輯器…

Dify中使用插件LocalAI配置模型供應商報錯

服務器使用vllm運行大模型&#xff0c;今天在Dify中使用插件LocalAI配置模型供應商后&#xff0c;使用工作流的時候&#xff0c;報錯&#xff1a;“Run failed: PluginInvokeError: {"args":{},"error_type":"ValueError","message":&…