一、前言
相信對于學習過單片機的同學對于調庫這個操作都不陌生,大家都是從調別人的庫階段過來的,今天看到一個評論說如果只會調庫到了公司后會發現自己啥都不是,其實這話說的一點也不假,如果你只會調庫的話你的單片機水平還停留在C語言階段,并不能稱為真正的單片機開發。
但是我們要有這么一個概念,調庫是自己編寫的開始,如果上來就給你講寄存器這些我相信很多初學者都接收不了,理解不了這寫寄存器到底在干啥,但是如果我們從調被人庫開始學習單片機我們就會對單片機有個初始概念,對于后面的學習非常有幫助,今天我們就看一下我們如何從調庫工程師成為真正的開發工程師。
二、什么是調庫?
如果你通過機構的培訓視頻,比如野火的STM32單片機開發視頻,相信你對于調庫并不陌生,調庫其實就是通過調用別人封裝好的庫函數,來實現自己的某些功能,不同的機構封裝出來的庫函數也有所不同,但是基本操作都大同小異,下面我們就以STM32調用固件庫實現點燈為例給大家進行講解。
我們先來看一個我們非常熟悉的結構體:
?
void?LED_GPIO_Config(void)//初始化相關的GPIO?第2個燈 { ?GPIO_InitTypeDef?GPIO_InitStruct; ?/*第一步:打開外設的時鐘(RCC寄存器控制)*/ ?RCC_APB2PeriphClockCmd(LED1_GPIO_CLK|LED2_GPIO_CLK,ENABLE); ? ?/*第二步:配置外設初始化結構體*/ ?GPIO_InitStruct.GPIO_Pin?=?LED1_GPIO_PIN; ?GPIO_InitStruct.GPIO_Mode?=?GPIO_Mode_Out_PP;//推挽輸出 ?GPIO_InitStruct.GPIO_Speed?=?GPIO_Speed_10MHz; ? ?/*第三步:調用外設初始化函數,把配置好的結構體成員寫到寄存器里面*/ ?GPIO_Init(LED1_GPIO_PORT,&GPIO_InitStruct); ? ?GPIO_InitStruct.GPIO_Pin?=?LED2_GPIO_PIN; ?GPIO_Init(LED2_GPIO_PORT,&GPIO_InitStruct); }
?
相信對于學習過STM32單片機的同學對于這個函數都不陌生,這個函數其實就是實現了對于一個GPIO的初始化,相信初學者并沒有思考過我們為什么要這么初始化呢?這里面的一些函數都有什么作用呢?他們是在哪個地方被封裝的呢?我們可不可以不按照這個函數的結構來寫呢?
帶著這些一文我們繼續往更深的層次去探索一下這些東西都是什么意思:
這里面用到了很多的宏定義,我們可以使用右鍵-->go to來向前查詢該宏定義在哪個地方進行定義的,例如我們對時鐘的宏定義LED1_GPIO_CLK 具體如下:
?
#define?LED1_GPIO_CLK?????????RCC_APB2Periph_GPIOC//時鐘 #define?LED1_GPIO_PORT????????GPIOC???????????????//端口 #define?LED1_GPIO_PIN?????????GPIO_Pin_2//pin?引腳 ? #define?LED2_GPIO_CLK?????????RCC_APB2Periph_GPIOC//時鐘 #define?LED2_GPIO_PORT????????GPIOC???????????????//端口 #define?LED2_GPIO_PIN?????????GPIO_Pin_3//pin
?
我們可以看到一些宏定義,例如LED1_GPIO_CLK被宏定義為RCC_APB2Periph_GPIOC,這里的RCC_APB2Periph_GPIOC就是官方固件庫中定義的時鐘,如果你想繼續研究RCC_APB2Periph_GPIOC代表什么意思,我們可以繼續右鍵-->go to
我們發現依然是宏定義,這里將RCC_APB2Periph_GPIOC宏定義成了((uint32_t)0x00000010),如果你想繼續了解((uint32_t)0x00000010)代表什么的話那就需要查看STM32的芯片手冊了,我們這里做一下簡單的講解。
關于GPIO的需要用到的寄存器如下:我們將0x10轉換為2進制為:1 0000我們可以看到第四位為1,其他位為0,查看芯片手冊可以發現第四位解釋如下:
發現這句話其實就是在使能I/O端時鐘C,和我們的使用是相同的。到這里我們就知道了從封裝的庫到底層寄存器中間經過了什么,當然這只是一個簡單的例子,實際會比此復雜很多。
三、如何不調庫點亮一個LED
通過固件庫我們可以看到如果想要控制一個GPIO大概需要以下幾步操作:
打開GOIO端口的時鐘
.配置IO口為輸出(控制CRL寄存器)
配置ODR寄存器
知道了我們需要進行的操作下一步我們就可以開始通過寄存器操作來控制一個LED了,具體代碼我直接給大家貼出來大家可以自己進行分析。
?
#define?rRCCAHB1CLKEN???*((volatile?unsigned?long?*)0x40023830)? ? #define?rGPIOF_MODER??*((volatile?unsigned?long?*)0x40021400)??? #define?rGPIOF_OTYPER?*((volatile?unsigned?long?*)0x40021404)? #define?rGPIOF_OSPEEDR??*((volatile?unsigned?long?*)0x40021408)? #define?rGPIOF_IDR??*((volatile?unsigned?long?*)0x40021410)? #define?rGPIOF_ODR??*((volatile?unsigned?long?*)0x40021414)? ? ? #define?rGPIOE_MODER??*((volatile?unsigned?long?*)0x40021000) #define?rGPIOE_OTYPER?*((volatile?unsigned?long?*)0x40021004) #define?rGPIOE_OSPEEDR??*((volatile?unsigned?long?*)0x40021008) #define?rGPIOE_IDR??*((volatile?unsigned?long?*)0x40021010) #define?rGPIOE_ODR??*((volatile?unsigned?long?*)0x40021014) ? ? #define?rGPIOA_MODER??*((volatile?unsigned?long?*)0x40020000) #define?rGPIOA_OTYPER?*((volatile?unsigned?long?*)0x40020004) #define?rGPIOA_OSPEEDR??*((volatile?unsigned?long?*)0x40020008) #define?rGPIOA_IDR??*((volatile?unsigned?long?*)0x40020010) #define?rGPIOA_ODR??*((volatile?unsigned?long?*)0x40020014) void?key_init() { ? ? ? ?rRCCAHB1CLKEN?|=?1?|?(1?<1); ? ? ? ?rGPIOA_MODER&=~(1|(1<<1)); ? ?rGPIOF_OSPEEDR?&=?~(1?|?(1?<1)?); ? ? ? ?rGPIOE_MODER&=?~(0x3f<<4); ? ??rGPIOE_MODER?&=?~(0x3f<<4); } void?led_init() { ? ?rRCCAHB1CLKEN?|=?(1?<5)?|?(1?<4); ? ? ?rGPIOF_MODER?&=?~((0x3?<18)?|?(0x3?<20)); ?rGPIOF_MODER?|=?(1?<18)?|?(1?<20);?? ? ?? ?rGPIOF_OTYPER?&=?~(?(1?<9)?|?(1?<10)); ? ? ? ?rGPIOF_OSPEEDR?&=?~((0x3?<18)?|?(0x3?<20)?); ? ?rGPIOF_ODR??|=??(1?<9?|?1?<10)?; ? ? ? ?rGPIOE_MODER?&=?~((0X3?<26)?|?(0X3?<28)?); ?rGPIOE_MODER?|=?(1?<26)?|?(1?<28); ? ?rGPIOE_OTYPER?&=?~(?(1?<13)?|?(1?<14)); ? ?rGPIOE_OSPEEDR?&=?~((0x3?<26)?|?(0x3?<28)?); ? ?rGPIOE_ODR??|=??(1?<13?|?1?<14)?; ? ? } ? ? void?delay(int?i) { ?int?v?=?i; ?while(v--); } ? void?led_on(int?i) { ?if?(i?==?0) ?{ ??rGPIOF_ODR?&=?~(1?<9); ??rGPIOF_ODR?|=?1?<10; ? ??rGPIOE_ODR?|=?(1?<13)?|?(1?<14); ?} ?else?if?(i?==?1) ?{ ??rGPIOF_ODR?|=?(1?<9); ??rGPIOF_ODR?&=?~(1?<10); ? ??rGPIOE_ODR?|=?(1?<13)?|?(1?<14); ?? ?} ?else?if?(i?==?2) ?{ ??rGPIOF_ODR?|=?(1?<9)?|?(1?<10); ? ??rGPIOE_ODR?&=?~(1?<13); ??rGPIOE_ODR?|=?1?<14; ?} ?else?if?(i?==?3) ?{ ??rGPIOF_ODR?|=?(1?<9)?|?(1?<10); ? ??rGPIOE_ODR?&=?~(1?<14); ??rGPIOE_ODR?|=?1?<13; ?} } ? int?main() { ?int?i?=?0; ?led_init(); ?key_init(); ?while(1) ?{ ?? ????if(!(rGPIOA_IDR&1)) ???????{ ?????delay(50);//消抖 ?????if(!(rGPIOA_IDR&1)) ?????{ ??????led_on(0); ?????} ????} ????else ????{ ????rGPIOF_ODR?|=?1?<9;//μ??e ????} ???if(!(rGPIOE_IDR&(1<<2))) ???????{ ?????delay(50); ?????if(!(rGPIOE_IDR&(1<<2))) ?????{ ?????led_on(1); ?????} ????} ????else ????{ ????rGPIOF_ODR?|=?1?<10; ????} ????if(!(rGPIOE_IDR&(1<<3))) ???????{ ?????delay(50); ?????if(!(rGPIOE_IDR&(1<<3))) ?????{ ?????led_on(2); ?????} ????} ????else ????{ ????rGPIOE_ODR?|=?1?<13; ????} ????if(!(rGPIOE_IDR&(1<<4))) ???????{ ?????delay(50); ?????if(!(rGPIOE_IDR&(1<<4))) ?????{ ?????led_on(3); ?????} ????} ????else ????{ ????rGPIOE_ODR?|=?1?<14; ????} ??? ?} }
?
上面的代碼實現的功能是通過循環掃描判斷按鍵是否被按下,如果按鍵被按下則對LED引腳輸出低電平從而點亮LED燈,這里用了四個按鍵和四個LED,方便大家理解之間的不同,引腳的定義如下:
?
LED的引腳定義為: LED0?->PF9 LED1?->?PF10 LED2->?PE13 LED3?->?PE14 按鍵引腳定義為: KEY0-->?PA0 KEY1-->?PE2 KEY2-->?PE3 KEY3-->?PE4
?
具體每個寄存器代表什么意思大家可以查看STM32的官方手冊,里面有詳細的介紹。沒有手冊的話可以看下面這篇文章,里面有常用的寄存器:https://www.cnblogs.com/jzcn/p/15775328.html
四、調庫與不調庫的區別
說到這兩者的區別也是我寫這篇文章的主要意圖,相信你打開這篇文章絕對不是來看不調庫是如何開發的,而是來看調庫開發和不調庫開發具體有哪些區別,為什么有現成的庫不用,非要自己去查寄存器,自己進行開發。
從應用角度講,寄存器相對來說是屬于更底層的,類似于驅動層,而固件庫則類似通過將寄存器封裝之后的應用層。相比之下,固件庫更像是包裝好給用戶的產品一樣,只需要我們使用就行了,讓封裝自己和寄存器打交道,而使用寄存器在使用時必須要清楚自己要操作那個一個寄存器,就很復雜,需要了解清楚寄存器的底層配置。
如果你學習過Linux的話想必你對分層的思想是有所了解的,雖然在單片機中分層思想的應用和Linux中的分層不太一樣,但也都是大同小異的。
STM32標準外設庫之前的版本也稱固件函數庫或簡稱固件庫,是?個固件函數包,它由程序、數據結構和宏組成,包括了微控制器所有外設的性能特征。
該函數庫還包括每?個外設的驅動描述和應用實例,為開發者訪問底層硬件提供了?個中間API,通過使用固件函數庫,無需深入掌握底層硬件細節,開發者就可以輕松應?每?個外設。
因此,使?固態函數庫可以大大減少用戶的程序編寫時間,進而降低開發成本。每個外設驅動都由?組函數組成,這組函數覆蓋了該外設所有功能。每個器件的開發都由?個通?API驅動,API對該驅動程序的結構,函數和參數名稱都進?了標準化。
這樣的操作既有好處又有壞處,對于毫無基礎的人來說它可以使我們的控制更加簡單,上手更容易,但是他也會造成我們接觸不到單片機的底層操作,可能你使用單片機干過很多的事,做過很多的項目,但是對于單片機的運行邏輯依然不清楚。
從專業角度來講,由于寄存器更底層,更需要用戶了解基本構成以及底層配置,所以說操作寄存器相對于固件庫顯得更加專業,相比之下,直接操作固件庫不需要了解那么多甚至不了解就可以直接開發,并不需要太多專業知識。
通過上面的分析我們可以總結出他們的優缺點:固件庫優點: 可以直接應用,操作更方便,開發迅速,適合新手入門。固件庫缺點: 因為操作固件庫,本質上也會對寄存器的操作,因為要通過封裝這一中間商,所以執行速度要比直接操作寄存器更慢,但是沒有寄存器移植那么方便。
所以我們可以從固件庫入門,之后再慢慢深入了解寄存器,了解相關知識,在我看來,了解更多底層的東西是有利無害的,更利于提升自己,可以懶,但是不能不會。
五、為什么要操作寄存器
回歸我們的中心,講了這么多我們到底該如何學習單片機呢?詳細這個問題在互聯網上都已經被談爛了。對于初學者應該如何入門應該學習哪些東西今天這篇文章我就不再討論了,今天要討論的內容是如果你已經入門了,也已經通過操作固件庫做了很多的東西,下一步你應該學習哪些東西。
如果你已經使用單片機做了很多的實驗,比如什么ADC采集、PWM波輸出這些操作你都用過了,并且感覺單片機你已經玩的爐火純青了,那么下面的東西對你應該很有用。
還有一點需要強調一下,如果未來你并不打算做單片機相關的工作的話那下面的東西你可以量力而行,可以作為了解的內容,并不用深入的了解。
大家學習51單片機的時候是不是常常進行一些寄存器操作,那為什么我們在32中就很少見到這些直接對寄存器進行操作呢?那是因為32的寄存器相比于51單片機要復雜很多,比如一個GPIO的操作可能就和很多的寄存器有關,我們很難通過一句話就可以控制一個GPIO,當然不這么干不代表不能這么干。
如果你接觸到單片機的高級開發(當然沒有這么一說,你可以理解成用單片機做一些產品)那么你的開發就會遇到瓶頸,從而限制你的開發,這也是很多單片機開發要求你一定要會寄存器操作。
對于經過系統培訓的開發者,單片機(MCU)或SoC的驅動開發,不管是使用各種庫還是直接上寄存器,都不成問題。
HAL庫函數或者固件庫都是ST開發的,也是人寫出來的代碼,既然是代碼,那就有存在BUG的可能,而且像這些經過ST調試過的代碼,更可能隱藏深層問題,這些都需要通過修改寄存器配置來調試定位。
所以這就可能在你的代碼里埋下了更深的炸彈,而且這些炸彈是埋藏的非常深的(一般的小bug是不會有的,畢竟那么多人使用)而這些bug一旦復現,你就會不知所措,完全不知道從何查起。
而且一般公司的MCU都不是你平時學的這些單片機,而是一些工業級的MCU,所以你可以想一想如果你一直使用的都是STM32的固件庫進行的開發,從來沒接觸過寄存器操作,或者根本都不知道怎么看芯片手冊,怎么操作寄存器,那么你怎么保證你們公司使用的MCU你就一定會操作呢?
所以對于STM32或者51這類單片機的定位你就把它當成學習使用的,你要通過這類簡單的、有豐富資料的單片機去入門,去學習。當你的學習內容達到一定程度之后就一定會接觸到寄存器這些操作,說實話寄存器操作也只是工作的基礎,最重要的是舉一反三,通過一個單片機學習到所有MCU操作的本質,這樣才能更好的在工作中使用,而不受單片機(MCU)型號的限制。
六、結語
對于單片機的學習我們可以使用單片機的固件庫入門,初步了解單片機操作的步驟,可以先不接觸寄存器,等到固件庫使用的非常熟悉之后可以轉戰寄存器了。
對于寄存器操作絕不是點個小燈就完了,你需要做的是知道如何查看芯片手冊,知道固件庫里的每個宏定義或者函數這么寫的依據是什么?如果讓你來寫一個固件庫你會怎么寫?
當你的水平能夠達到對STM32的寄存器操作已經非常6的話你可以嘗試幾款工業級的MCU,例如工業非常常用的TC397,這個MCU在車載行業用的非常多,可以嘗試一下,不過治療可能不太好找,如果遇到問題的話就需要自己琢磨了,這也是一種進步。
審核編輯:湯梓紅
評論
查看更多