# 初學者入門書有沒有推薦的
大學學的是單片機89C51,基礎不是很好,也就一知半解,能在百度的幫助下修改代碼,實現程序功能,下學期大四,因為現在很多公司都用的是嵌入式STM32 招聘要求上寫的也是要熟悉STM32和Linux,所以我覺得去認真學習一下STM32,在一開始的時候,很懵圈,直到看到了一本嵌入式入門文章 《零死角玩轉STM32》 才有一些了解STM32,下面是書中的語段,想把一些學習到的東西記錄下來。
很感謝在網絡上無私奉獻,分享經驗的前輩,雖然我感覺我還沒入門,但如果只靠一些書籍,我很難自己理解,下面的文章都是文庫里的資料,我只想再學習一次,記錄下來,如果誰知道這本書的,和我說一下哪里能買,淘寶上找不到這本書

# 一開始通過51來認識STM32
51是嵌入式的入門級的經典MCU,結構簡單,易于教學,但是現在市場產品競爭愈發激烈,對成本及其敏感,相應地對MCU的要求也更高了,所以STM32的出現,不是偶然是一種趨勢,
本章要結合 《STM32中文參考手冊》和《CM3權威指南CnR2》 一起閱讀,效果更佳
# 用寄存器點亮LED
## 51點亮LED燈
如何用51來點亮一個LED
在硬件上,我們假設在51單片機的P0口的第0位接一個LED,負邏輯亮,代碼上,我們會這樣寫
P0=0XFE;
關掉LED則是
P0=0XFE;
這里面我們用的是總線的操作方法,即是對P0口的8個IO口同時操作,但起作用的只是P0^0,除了總線操作,我們還學習過位操作,利用51編譯器的關鍵字sbit,我們可以定義一個位變量:
```
Sbit LED=P0^0;
```
那么給0亮, 給1滅
*
*LED=0; //亮
LED=1;//滅**
### 寄存器

在點亮LED的時候,我們都是用操作寄存器的方式來實現的,那這個寄存器到底是什么?為什么我們可以直接操作P0口?
以**STC89C51**為例,該單片機主要由51內核,外設IP,總線三大部分組成,內核由Inter公司生產的,總線是用來連接內核和外設的接口單元
**寄存器則是內置于各個IP外設中,是一種用于配置外設功能的存儲器,就是一種內存,并且有相對應的地址**。學過C語言我們就知道,要操作這些內存就可以使用C語言中的指針,通過尋址的方式,來操作這些具有特殊功能的內存-存儲器,比如 P0口對應的地址是0X80 ,那么我們需要修改0X80這個地址對應的內存的內容的話,按常理可以這樣操作:
```
*(0X80) =0XFE
```
可我們這樣編譯的時候,編譯器會報錯,在51里面只能通過Sfr和sbit這倆個關鍵字來是實現寄存器的映像,不能直接操作寄存器對應的地址,這是51不同于STM32的地方
51單片機的這些寄存器位于地址80H—FFH中,對應128個地址,但不是每個地址都是有效的,51有21個,52系列則有26個,其他的都是保留區。
### 寄存器映射
實際上我們在編程的時候并不是通過指針來操作寄存器的,二十直接給P0,P1這些端口寄存器賦值,那么這些外設資源是如何與地址一一對應的關系,這也得益與51**特有**的倆個關鍵字 SFR和SBIT ,其他單片機沒有,只能用其他的方式來實現寄存器映射,這倆個關鍵字幫我們實現了所有寄存器的定義,所以我們才可以像操作普通變量來控制寄存器,所以,我們一開始的點燈LED的代碼,全貌應該是
sfr P0 =0X80;
P0=0XFE;
為了方便起見,我們可以把寄存器映射全部寫好封裝在一個頭文件里面,不用每用一個寄存器就定義一次,其實這方面的工作,不用我們做,我們在編程的時候都會在開始的地方添加一個頭文件:`#include<reg51.h>`這個頭文件已經實現了全部寄存器的定義,該文件是keil自帶的
在安裝目錄Keil \C51\INC 下面可以找到,這個文件實現了字節寄存器和位寄存器的定義

### 啟動文件-STARTUP.A51
還有一個就是啟動代碼,這是大多數人容易忽略的地方,我們主要總結一下它的功能,不詳細講解里面的代碼
單片機在上電之后,首先執行的是啟動文件,而不是我們通常看到的main函數,我們新建51工程的時候會有一個提示,

是否添加啟動代碼,代碼主要實現了以下功能:清除內部數據存儲器,清除外部數據存儲器,清除外部頁儲存器,初始化Small模式下的可重入棧和指針,初始化large模式下可重入棧和指針,初始化compact模式下的可沖入棧和指針,初始化8051硬件棧指針,傳遞初始化全局變量的控制命令或者在沒有初始化全局變量時給main函數傳遞命令,然后程序就跳轉到main函數,來到我們熟悉的C語言世界
## STM32
現在我們對比51點亮LED的方法,我們先用操作寄存器的方法用STM32點亮一個LED,然后再一步步完善代碼,構建最簡單的庫函數,讓我們知道庫函數是怎么建立起來的
1. 啟動文件
新建工程,把工程放入事先建好的文件夾中,然后在工程目錄下添加startup_stm32f10x_hd.s
STM32的啟動文件主要實現了:
1.設置初始SP。
2.設置初始PC=Reset_Handler
3.設置向量表入口地址,并初始化向量表。
4.調用庫函數SystemInit ,把系統時鐘配置成72M,SystemInit 在庫文件system_STM32F10.C定義.
5.跳轉到標號_mian,最終來到C的世界,這里我們先去除繁枝細節,挑重點的講,主要理解第四和第五點,在啟動文件的147—155行,是復位處理函數,代碼如下
```
1 ;Reset handler
2 Reset_Handler PROC
3 EXPORT Reset_Handler
4 IMPORT _main
5 IMPORT SystemInit
6 LDR R0,=SystemInit
7 BLX R0
8 LDR R0,=main
9 BX R0
10 ENDP
```
簡單解釋一下這10行代碼
第一行 是程序注釋,在匯編中注釋是“ ;”
第二行,是定義了一個子程序 Reset_Handler ,而 PROC和最后的ENDP 配合使用,是子程序定義偽指令
一般用法

第三行 EXPORT 表示 該子程序可供其他模塊調用
關鍵字,【WEAK】表示弱定義,如果編譯器發現在別處定義了同名的函數,則在連接時用別處的地址進行鏈接,如果其他地方沒有定義,編譯器也不報錯,以此處地址進行鏈接
第四行 和 第五行IMPORT 說明SystemInit和_main 這倆個標號在其他文件,在鏈接的時候需要到其他文件去尋找
SystemInit 在庫文件 system_stm32f10x.c實現,用來初始化,STM32的一系列時鐘,把系統時鐘設置為72MHZ,STM32的時鐘比51單片機復雜,需要經過一系列的配置才能達到穩定運行的狀態
_main 其實不是我們定義的,當編譯器編譯時,只要遇到這個標號就會定義這個函數,該函數的主要功能是:負責初始化棧,堆,配置系統環境,并在最后跳轉到用戶自定義的main函數
第六行 把SystemInit的地址加載到寄存器 R0
第七行 程序跳轉到R0中的地址執行程序,之后系統的時鐘被設置為72MHZ
第八行把_main 的地址加載到寄存器R0
第九行程序跳轉到R0中的地址執行程序,執行完畢之后就去我們熟悉的C世界
第十行表示子程序結束
總結一下,Reset_Handler 這個函數執行了倆個函數的調用,一個是SystemInit ,把系統的時鐘設置成72M,令一個_main,初始化系統環境,最終調用C的main
**等會我們的主要任務還是點亮 LED 的時候采用最簡單的方法,直接使用內部的LSI時鐘,(8MHZ)作為主時鐘即可,不使用外部時鐘LSE **
_main 函數由編譯器生成,負責初始化棧,堆等,,并在最后跳轉到用戶自定義的main函數
3. mian.c
先編寫一個 main 函數

這時候出現了一個錯誤
```
Undefined symbol SystemInit (referred from startup_stm32f10x_hd.o)
```
SystemInit 沒有定義,從分析啟動文件時,我們知道,Reset_Handler 調用了該函數用來初始化系統時鐘,而該函數實在庫文件 System_stm32f10x.c中實現的,我們重新寫一個函這個的函數也可以,把功能完整的實現一邊,但為了方便起見,我們在main文件里面定義一個SystemInit空函數,為的是騙過編譯器,把這個錯誤去掉,關于配置系統時鐘我們在后面再寫簡單的代碼

這時候編譯沒有錯了,還有一個方法是再啟動文件中把關于SystemInit函數的代碼注釋掉也可以,代碼如下:

4. 控制IO口
下面我們從三個方面來講解STM32的IO口在控制LED時,和51的區別,關于STM32DE IO寄存器的介紹,我們可以看《STM32中文參考手冊》第八章即可,下面涉及的IO寄存器均來自這一章的第二小節
### 寄存器
#### 1.電平控制
51單片機的IO口如果要輸出1和0,可以直接賦值,不用控制其他寄存器
而STM32的IO口比較復雜,如果要輸出1和0,則要通過控制;端口輸出數據寄存器ODR來實現 ,ODR 是 Output data register 的簡寫,在STM32里面,其寄存器的命名都是英文的簡寫,很容易記住,從手冊上我們知道ODR是一個32位的寄存器,低16位有效,高16位保留,低16位對應著IO 0- IO 16 ,只要往對應的位置寫入0或者1 就可以輸出低或者高電平

PB0輸出低電平,代碼如下
```
GPIOB_ODR = 0<<0
```
這時候編譯,會有一個錯誤,說 GPIOB_ODR 沒有定義,不過我們確實沒有定義,在51單片機中,我們可以直接往P0口賦值,那是因為在reg51.h這個頭文件里面實現了P0口這個寄存器的映像,用的是51特有的SFR來實現的,但是STM32不一樣沒有,sfr,所以只能用其他的方式來實現寄存器映像,因為寄存器實際上就是具有特殊功能的內存,那么我們可以通過宏定義來實現寄存器映像,其實ST的庫函數中用的也是這種方法,從手冊中我們看到ODR寄存器的偏移是:0CH,(上圖左上角),這個偏移地址是基于端口的起始地址而言的,在STM32中,每個外設都有一個起始地址,叫外設基地址,外設的寄存器,就以這個基地址位標準按照順序排列,跟結構體里的成員差不多。
在手冊的第二張,2.3 存儲器映像中,可以查找所有外設的基地址,如下
其中 GPIOB的起始地址位 : 0X40010C00 ,從這個地址,我們可以算出GPIOB_ODR寄存器的地址位:0x40010C0C
0X4001 0C00 + (地址偏移)0X 0C =(寄存器地址)
所有定義代碼如下:
```
#define GPIO_ODR *(volatile unsigned long *) 0x40010C0C
```
**long是32位整型,unsigned指無符號數,左邊的*表示取內容
volatile表示易變的,告訴編譯器不要優化,這個地址的內容不一定是在程序中改變的。
volatile unsigned long * 表示將后面跟的內容轉化成一個指針,并且是指向一個易變的無符號整數。
左邊再加個 * ,表示取該指針指向地址的內容。
總的意思是取那個內存單元(內存地址0x40010C0C)里存的數,并將這個數轉化為無符號整數**
所以有了這個寄存器定義,我們就可以直接控制操作FPIOB_ODR了
#### 2.方向控制

雖然配置了ODR,但這個時候還不能點亮LED,因為STM32的IO口還要配置方向,這個由端口配置寄存器來控制,端口配置寄存器分為高低倆個,每4bit控制一個IO口,所以端口配置寄存器:CRL控制這個IO口的低8位,端口配置高寄存器: CRH控制這個IO口的高8bit,在4位一組的控制位中,CNFy【1:0】用來控制端口的輸入輸出,MODEy【1:0】用來控制輸出模式的速率,即輸出時,IO電平翻轉的速度
輸入有三種模式。輸出有四種模式,我們在控制LED的時候,選擇通用推挽輸出。
輸出速率有三種模式:2M,10M,50M,這里我們選擇2M
同GPIOB_ODR 一樣,我們也可以算出FPIO_CRL的地址為:0X40010C00 那么設置PB0為通用推挽輸出,輸出速率為2M的代碼則如下
`#define GPIOB_CRL *(volatile unsigned long *) 0x40010C00
//配置PB0位通用推挽輸出,輸出速率為2M
GPIOB_CRL = 2<<0 | 0<<0`
這個代碼我又查了了一下
GPIOB->CRL = 0x00000002;
//PB0:CNF0[1:0]=00, MODE0[1:0]=10
這樣就好理解多了,主要還是要學會看手冊
#### 3.時鐘控制


當我們設置了IO口的方向,并在相應的輸出寄存器里輸入了值得時候,覺得現在總算可以點亮LED了吧,其實還差最后一步
STM32外設很多,未來降低功耗,每個外設都對應了一個時鐘,在系統復位的時候這些時鐘都是關閉的,想要這些外設工作,必須把相應的時鐘打開
而STM32的所有外設的時鐘由一個專門的外設來管理叫RCC(reset and clock contrlo) 在手冊第六章
STM32的外設因為速率的不同,分別掛載在三條總線上,AHB,APB2,APB1,AHB位高速總線,APB2次之,APB1再次之,所有的IO口都掛載在APB2總線上,屬于高速外設,時鐘有APB2外設時鐘使能寄存器 (RCC_APB2ENR )來控制,其中PB端口的時鐘由該寄存器的位3寫1使能。
同ODR 和CRL ,我們可以算出RCC_APB2ENR的地址
0x40021018
```
#define RCC_APB2ENR *(volatile unsigned long *) 0x40021018
//開啟端口B
GPIOB_CRL = 1<<3;
手冊上位3為 B端口使能端 給1 使能
```
如果你足夠細心,你會發現我們雖然開了端口時鐘,那么這個時鐘到底多大呢?又是用哪里來的呢
如果我們用的是庫,那么會有一個庫函數 SystemInit,會幫我們把系統時鐘設置位72M 但現在我們沒有使用庫,那現在時鐘是多少呢?答案是8M,當外部HSE沒有開啟或者出現故障的時候,系統時鐘由內部低速時鐘LSI提高,現在我們確實沒開啟HSE,所有系統默認的時鐘位LSI=8M,至于更深入的細節,在后面的RCC時鐘樹中在詳細分析,如果你想自己先嘗嘗鮮,那么可以看RCC外設中的:時鐘控制寄存器 (RCC_CR)和時鐘配置寄存器(RCC_CFGR)這倆個寄存器即可
#### 4.水到渠成
我們現在控制了電平,配置了方向,開啟了時鐘,經過這三部,我們總算可以控制一個LED了,比起51直接輸出點評,控制STM32的IO多了倆步:即配置方向可開啟時鐘,比起AVR和PIC這倆種單片機則多了開啟時鐘這一步
下面是一個完整的用STM32控制一個LED代碼
```
#define RCC_APB2ENR *(volatile unsigned long *)0x40021018
#define GPIOB_CRL *(volatile unsigned long *)0x40010C00
#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C
int main (void)
{
//開啟端口B 的時鐘
RCC_APB2ENR |= 1<<3;
//配置PB0位輸出速率為2M,通用推挽輸出
GIOPB_CRL |=(2<<0)|(0<<2);
//PB0輸出低電平,點亮LED
GIOPB_ODR = 0<<0 ;
}
void SystemInit (void)
{
}
```
### 再接再厲
學習STM32存在一個用庫好還是用寄存器好的爭議,就好像編程用匯編好還是用C好一月,其實孰優孰劣,在市場上自有定論,用戶群說明一切
雖然我們上面用寄存器點亮了LED,乍一看好像代碼也很姜丹,但是我們別僥幸以后就一直用寄存器開發了,在用寄存器點亮LED的時候,我們是否發生STM32的寄存器都是32位的,在配置時非常容易出錯,而且代碼還很不好理解,所以學習STM32最好的方法是用庫,然后在庫的基礎上了解底層,看遍所有寄存器
這里我們講一下,關于GPIO庫,其他的外設我們直接參考庫學習即可,不必自己寫
#### 定義外設寄存器結構體
上面我們在操作寄存器的時候,操作的是寄存器的絕對地址,如果每個寄存器都這樣操作的話,那將非常麻煩
我們考慮到外設寄存器的地址都是基于外設地址的偏移地址,都是在外設基地址上逐個增加的,每個寄存器占了32個或16個字節,這種方式和結構體里面的成員類似,所以我們可以定義一種外設結構體,結構體的地址等于外設的基地址,結構體的成員等于寄存器,成員的排列順序跟寄存器的順序一月,這樣我們操作寄存器的時候,就不用每次都找到絕對地址,只要找到外設的基地址就可以操作外設的全部寄存了
下面我先定義一個GPIO 寄存器結構體,結構體里的成員全是GPIO的寄存器,按寄存器的偏移地址從低到高排列
//GPIO 寄存器結構體
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_TypDef;
在手冊 第8.2我們可以找到7個寄存器描述,在點亮LED的時候,我們只用了CRL和ODR這倆個寄存器,至于其他寄存器功能,自行看手冊
在GPIO結構體中,我們用來倆個數據類型,一個是 uint32_t ,表示無符號的32位整型,因為GPIO的寄存器都是32位的,這個類型聲明在標準頭文件stdint.h里面,我們在程序上只要包含這個頭文件即可
另外一個是_IO,這個是我們自己定義的,原型是volatile ,作用是高速編譯器不要因優化而省略此指令,必須每次都直接讀寫其值,這樣就可以確保每次讀寫或者寫寄存器都真正執行到位
為了這倆個數據類型,我們添加一下代碼
```
#includu <stdint.h>
#define _IO volatile
```
#### 外設聲明
寄存器結構體已經定義好了,GPIO端口分為A-G,每個端口都含有GPIO_TypDef 類型的指針,然后我們就可以根據端口名(實際上現在是結構體指針)來操作各個端口的寄存器,代碼實現如下:
```
//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)
```
對于其他外設,我們也可以這樣將外設的名字定義為一個外設寄存器結構體類型的指針,這里我們只講GPIO

這里我蠻奇怪的第一個地方,還在探索,為什么GPIO端口F和G的起始位置一樣
#### APB1,APB2,AHB 總線基地址

這張圖在之前有,只是我拿出來說一下,總線的位置在這里然后現在先定義基地址,然后在基地址上加入偏移地址就可以

直接上圖


#include <stdint.h> //點一個LED燈
#include <stm32f10.h>
#define _IO volatile
//GPIO 寄存器結構體
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_TypDef;
//RCC 寄存器結構體
typedef struct(
_IO uint32_t CR;
_IO uint32_t CFGR;
_IO uint32_t CLR;
_IO uint32_t APB2RSTR;
_IO uint32_t APB1RSTR;
_IO uint32_t AHBENR;
_IO uint32_t APB2ENR;
_IO uint32_t APB1ENR;
_IO uint32_t BDCR;
_IO uint32_t CSR;
) Rcc_TypDef;
//總線基地址
#define PERIPH BASE ((uint32_t)0x40000000) //外設基地址
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PEPIPH_BASE+0X10000)
#define AHBPERIPH_BASE (PEPIPH_BASE+0X20000)
//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)
//RCC
#define RCC_BASE ( AHBPERIPH_BASE+0X1000)
//外設聲明
#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 RCC ((RCC_TypeDef*) RCC_BASE)
//
#define RCC_APB2ENR *(volatile unsigned long *)0x40021018
#define GPIOB_CRL *(volatile unsigned long *)0x40010C00
#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C
int main (void)
{
//開啟端口B 的時鐘
RCC->APB2ENR |= 1<<3;
//配置PB0位輸出速率為2M,通用推挽輸出
GIOPB->CRL |=(2<<0)|(0<<2);
//PB0輸出低電平,點亮LED
GIOPB->ODR = 0<<0 ;
}
void SystemInit (void)
{
}
這里我們用的是宏定義,之前我們用的是結構體定義,所以有沒有看出來在main函數的代碼有一些不一樣
用結構體可以直接操作,用宏定義還要一個個的找到寄存器的絕對地址進行重新定義
比如開時鐘
RCC->APB2ENR |= 1<<3;
這章節本來后面還有一些是關于軟件的實際操作的,但是因為剛剛學還沒買開發板,學生一名,想多看看,對比一下,找一下性價比高的開發板,如果有這本書的,電子版的或者實體銷售店的··留言一下, 這本書第一次看的時候感覺剛好把我學的51和想學的STM32結合在一起,更好理解了
上面是我自己選重點給自己看的
https://wenku.baidu.com/view/193e26de900ef12d2af90242a8956bec0975a58c.html 原文件
指針的概念理解
https://www.cnblogs.com/Waming-zhen/p/4353963.html
|