如果不會寄存器開發(fā)而陷入瓶頸, 那么本文將會有較大幫助
如果不會寄存器開發(fā)而陷入瓶頸,那么本文將會有較大幫助

0.緒論
對于經過系統(tǒng)培訓的開發(fā)者.單片機或SoC的驅動開發(fā),不管是使用各種庫還是直接上寄存器,都不成問題.
可是很多非專業(yè)但是需要干嵌入式或單片機工程的人,比如機械,車輛工程,物理,或是其他的一些專業(yè).有時候這些學生需要搞比賽,做項目,不可避免的要用到一些單片機(由于種種原因,現(xiàn)在幾乎都會首選STM32).但是缺乏系統(tǒng)訓練的學生往往無法看懂寄存器版本的例程,或是別人的開源項目.自己寫的話更是不知道如何開始.或者編譯完成后總是出現(xiàn)莫名的問題.這給大家?guī)砹藰O大的困難.
同時互聯(lián)網(wǎng)上大部分教程都是轉載來轉載去,各種差異和版本都有.很多人即使看過了也是一頭霧水.所以應一個同學要求,大概寫一下寄存器的一些操作.
本教程將會使用"學生用戶體量龐大"的STM32F1xx系列的單片機作為例子.但是不必擔心.所有的寄存器操作都是共通的.并不會有本質的差異.希望你能快速閱讀完并且理解完,然后查閱自己需要的資料.不管是什么東西,這種方式都是通用的.
嵌入式軟件開發(fā)具有一個比較龐大的知識體系.限于作者的時間和水平,本文不可能講太多東西.但是如果你只是因為不會寄存器開發(fā)而陷入瓶頸,那么本文將會有較大幫助(畢竟這個是寫文章的目的).
理解本文需要一些基礎,大概包括: c/c++,一些數(shù)學,計算機基礎知識.大多數(shù)理工類專業(yè)都會有相關的課程的.如果確實存疑可以即時搜索.
讓我們先記住開發(fā)的正確方式: 摘抄借鑒,修改糅合,以實現(xiàn)功能為目的,以別人的代碼為基礎.
只要不涉及什么知識產權的話,這樣做是完全沒有問題的.畢竟不需要把很多基礎性的東西寫來寫去.所以請大膽的找開源項目,盡可能在基礎上改,而不是從零開發(fā).不廢話了,開始正事.
比如十進制下的0.1就是二進制的無限不循環(huán)小數(shù).上面那個例子也是.
著名的 IEEE754 float浮點數(shù)標準導致的bug:在很多語言中, 0.1+0.2≠0.3
就是因為0.1是二進制無限循環(huán)小數(shù)的原因.但是存儲器位寬不能是無窮的.所以產生舍入誤差.
所以在很多項目中,為了實現(xiàn)當兩個值相等時觸發(fā)什么函數(shù),往往不會直接寫相等,而是兩者的差值小于多少時即生效
floata,b;........if(a==b){
//這種寫法不建議}........if(abs(a-b)0.00001){
//一般這么寫}
二進制與十/十六進制的轉換
剛才那個方法就可以直接算.還有其他算法這里不講.先讓我貼一個表格:
BIN
DEC
HEX
0000
0
0
0001
1
1
0010
2
2
0011
3
3
0100
4
4
0101
5
5
0110
6
6
0111
7
7
1000
8
8
1001
9
9
1010
10
A
1011
11
B
1100
12
C
1101
13
D
1110
14
E
1111
15
F
BIN是二進制, DEC是十進制, HEX是十六進制.都是英語簡寫.
十六進制和二進制可以直接換算.方法是每四位二進制看作一個十六進制
比如
0110 1101 1011 1001
6 D B 9
轉換表背過的話讀代碼快些.因為一般情況下,為了讓一行代碼看的不至于太長,人們會用十六進制代表二進制.尤其是對于擁有32位cortex-M3內核的STM32F1系列單片機,一次寫一個三十二位數(shù)屬實太冗長.
c/c++中,默認寫的數(shù)字都是十進制.二進制應該是0b開頭,比如0b00101100,而十六進制是0x開頭,比如0x3C.
//一般這么寫GPIOB->CRL&=0x00440000;//這么寫就不太美觀了GPIOB->CRL&=0b00000000010001000000000000000000;
數(shù)學差不多了.開始正文.
2. c/c++語言基礎
a+b; //加法
a-b; //減法
a*b; //乘法
a/b; //除法,所有計算需要注意整型(整數(shù))和浮點型(小數(shù))的運算區(qū)別.如有疑問自行搜索
a%b; //求模.就是小學學的余數(shù). 14÷4=3 ... 2 這里 14mod4=2, 14/4=3. float(14)/4=3.5f
a</左移.將a看為二進制數(shù),整個向左移動b位.比如 00000110<就是 00011000.當溢出的時候會發(fā)生什么呢?
a>>b; //右移.和上面一樣的功能.
a&b; //按位求與(AND),比如 11010010
// & 01010011
// ------------
// 01010010
a|b; //按位取或(OR)
~a; //按位取非(NOT)
!a; //邏輯非(注意和上面的區(qū)別)
c+=a; //相當于 c=c+a;其他算符同理.
重點看左右移和位操作(與,或,非等).
3.我們配寄存器(register)到底是在配置什么
首先,你的最終目的都是使用單片機的GPIO(general pin input & output)讀取/輸出一個高電平還是低電平.不管是諸如I2C, SPI的通信,還是按鍵讀取,亮燈報警,說到底都是高低電平的控制或探測.
ADC輸入的是模擬(Analog)信號,但是會被轉化為數(shù)字(Digital)信號,一樣是高低電平.這里暫且不談
外設,時鐘,或者一些功能的使能及配置一樣是通過寄存器的.原理相似.畢竟芯片集成電路也是電路,而且是數(shù)字電路.暫時不深入.
所以,為了讓某一個Pin輸出電平,或者使能一個通道,我們可以用0或者1實現(xiàn).
但是代碼終歸是代碼,不是魔法.為了使需求生效,單片機將每一個需要控制的量,賦予一個地址.在電路層面上實現(xiàn)相關的功能綁定.用戶只需通過給這個地址去寫一個值,就相當于控制了需要控制的東西.易于計算,我們的32位處理器最大可以尋址4GB的內存空間.
注意:并不是每一個地址都是有真實物理地址對應的.換而言之,一個地址可能指向的是真實的內存,也可能并不是真實存在的內存.不過訪問這個地址相當于控制了被控量一樣.此時該地址可以看作控制量的一個句柄(handler).
"使能"的意思是enable.反義詞是"禁用(disable)".計算機相關的詞匯總是這么的看不懂字面意思.看英語就明白了.
內存是存儲數(shù)據(jù)的地方.任何電子信息數(shù)據(jù)都可以看做01串. 32位機的最小存儲單元是DWORD(雙字, Double Word),包含32比特位. 32位機的內存單元可以看作一個個存儲32比特位的小倉庫.為了找到所需要的數(shù)據(jù),需要給這些倉庫編號.這個所謂的編號就是地址(Address).存儲的值是地址值的變量叫做指針(pointer).比如一個倉庫放著一個記錄某個貨物的多個存放倉庫的編號,那么這個內存里的數(shù)據(jù)就是一個指針.計算機無法分辨哪寫是指針,哪些是數(shù)據(jù).這需要人去完成.
庫函數(shù)是寄存器的封裝.不管是ST出的HAL硬件抽象層庫,還是標準庫STL,都是封裝而已.本質上是宏定義替換和一些函數(shù).宏定義在編譯(compile)階段完成.不占用寶貴的存儲空間.函數(shù)本身會被編譯為代碼,所以會占用空間.盡量避免在庫中使用函數(shù).當然用戶代碼肯定無所謂.
為什么要用庫函數(shù)呢?比如
GPIO_InitTypeDef GPIO_Initure;
GPIO_Initure.Pin=GPIO_PIN_13;
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOB,&GPIO_Initure);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET);
這一段代碼,不加注釋你也知道他的意思是初始化PB13為推挽輸出.
但是如果寫成了寄存器版本的你可能就看不懂了.
GPIOB->CRH&=0XFF0FFFFF;
GPIOB->CRH|=0X00300000;
GPIOB->ODR|=1<
現(xiàn)在會簡要介紹寄存器的配置方法.你能搜到這個文章肯定是因為你找見的開源代碼使用的是寄存器版本. 正常人的話,能找見庫函數(shù)絕對是不會用寄存器版本的代碼的.
寄存器版本的代碼實際上也是封裝,比如上述代碼實際上也可以直接寫成
(uint32_t *)(((((uint32_t)0x40000000) + 0x10000) + 0x0C00)+0x01)&=0XFF0FFFFF;
(uint32_t *)(((((uint32_t)0x40000000) + 0x10000) + 0x0C00)+0x01)|=0X00300000;
(uint32_t *)(((((uint32_t)0x40000000) + 0x10000) + 0x0C00)+0x04)|=1<
//這一段代碼可能不對
上面就是直接通過指針操作寄存器的方式.寄存器宏定義和結構體相當于,先找到GPIOB的開頭地址,然后加上結構體的附加地址偏移(去操作某一個功能,比如CRH寄存器).最后給這個地址寫入一串二進制值去控制相應的東西.
說到底還是去給某些比特上寫高低電平.
類似于GPIOB->CRH這種,實際上是定義的一個結構體.
typedefstruct{
__IOuint32_tCRL;
__IOuint32_tCRH;
__IOuint32_tIDR;
__IOuint32_tODR;
__IOuint32_tBSRR;
__IOuint32_tBRR;
__IOuint32_tLCKR;}GPIO_TypeDef;
通過結構體的位關系以及各種宏定義替換,可以實現(xiàn)地址的編譯時自動確定.
寄存器的封裝好在對于實際內存位置不同的芯片,移植只需更改寄存器宏定義映射即可.而且已經很容易讀了.
4.常見的寄存器配置初始化(使用STM32F1xx系列舉例子)
我們寫驅動,最常用的就是GPIO的配置了.我們就舉一些例子.
RCC寄存器
RCC寄存器一般用來配置RCC時鐘相關的代碼.比如我要使用PA0的話,我必須開啟PA的時鐘通道才行.時鐘相當于處理器的心跳,沒有心跳當然是死的.但是沒理由的開啟時鐘會帶來額外的EMI干擾和整機功耗.原理暫且不談,實際危害就是干擾大了,耗電多了.尤其針對于一些對功耗要求嚴格的場合,例如手環(huán)手表,應該盡可能在待機時關閉能關閉的所有時鐘.用來省電(比如有的智能手表升級系統(tǒng)后待機變久了,可能就是這個原因.我們說的固件(firmware)升級實際上指的就是把新寫的代碼下載到了老設備上).
舉個例子, RCC時鐘的配置如下
RCC->APB2ENR|=1</使能PORTC時鐘
RCC->APB2ENR|=1</使能PORTB時鐘
RCC->APB2ENR|=1</使能PORTA時鐘
具體使能哪一個位是控制誰,需要查表(除非你背過了).
如果是需要快速出項目,不管其他的,可以省事,直接使能所有時鐘.但是不建議這么干.