1)實驗平臺:正點原子stm32f103戰(zhàn)艦開發(fā)板V4
2)平臺購買地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套實驗源碼+手冊+視頻下載地址: http://www.openedv.com/thread-340252-1-1.html
第三十章 ADC實驗
本章,我們將介紹STM32F103的ADC(Analog-to-digital converters,模數(shù)轉(zhuǎn)換器)功能。我們通過四個實驗來學(xué)習(xí)ADC,分別是單通道ADC采集實驗、單通道ADC采集(DMA讀?。嶒?、多通道ADC采集(DMA讀?。嶒灪蛦瓮ǖ繟DC過采樣(16位分辨率)實驗。
本章分為如下幾個小節(jié):
30.1 ADC簡介
30.2 單通道ADC采集實驗
30.3 單通道ADC采集(DMA讀取)實驗
30.4 多通道ADC采集(DMA讀?。嶒?br> 30.5 單通道ADC過采樣(16位分辨率)實驗
30.1 ADC簡介
ADC即模擬數(shù)字轉(zhuǎn)換器,英文詳稱Analog-to-digital converter,可以將外部的模擬信號轉(zhuǎn)換為數(shù)字信號。
STM32F103系列芯片擁有3個ADC(C8T6只有2個),這些ADC可以獨立使用,其中ADC1和ADC2還可以組成雙重模式(提高采樣率)。STM32的ADC是12位逐次逼近型的模擬數(shù)字轉(zhuǎn)換器。它有18個通道,可測量16個外部和2個內(nèi)部信號源,其中ADC3根據(jù)CPU引腳的不同其通道數(shù)也不同,一般有8個外部通道。ADC中的各個通道的A/D轉(zhuǎn)換可以單次、連續(xù)、掃描或間斷模式執(zhí)行。ADC的結(jié)果可以以左對齊或者右對齊存儲在16位數(shù)據(jù)寄存器中。
STM32F103的ADC主要特性我們可以總結(jié)為以下幾條:
1、12位分辨率;
2、轉(zhuǎn)換結(jié)束、注入轉(zhuǎn)換結(jié)束和發(fā)生模擬看門狗事件時產(chǎn)生中斷
3、單次和連續(xù)轉(zhuǎn)換模式
4、自校準(zhǔn)
5、帶內(nèi)嵌數(shù)據(jù)一致性的數(shù)據(jù)對齊
6、采樣間隔可以按通道分別編程
7、規(guī)則轉(zhuǎn)換和注入轉(zhuǎn)換均有外部觸發(fā)選項
8、間斷模式
9、雙重模式(帶2個或以上ADC的器件)
10、ADC轉(zhuǎn)換時間:時鐘為72MHz為1.17us
11、ADC供電要求:2.4V到3.6V
12、ADC輸入范圍:VREF–≤VIN≤VREF+
13、規(guī)則通道轉(zhuǎn)換期間有DMA請求產(chǎn)生
下面來介紹ADC的框圖:
圖30.1.1 ADC框圖
圖中,我們按照ADC的配置流程標(biāo)記了七處位置,分別如下:
① 輸入電壓
在前面ADC的主要特性也對輸入電壓有所提及,ADC輸入范圍VREF–≤VIN≤VREF+,最終還是由VREF–、VREF+、VDDA和VSSA決定的。下面看一下這幾個參數(shù)的關(guān)系,如圖30.1.2所示:
圖30.1.2 參數(shù)關(guān)系圖
從上圖可以知道,VDDA和VREF+接VCC3.3,而VSSA和VREF-是接地,所以ADC的輸入范圍即0~3.3V。
② 輸入通道
在確定好了ADC輸入電壓后,如何把外部輸入的電壓輸送到ADC轉(zhuǎn)換器中呢,在這里引入了ADC的輸入通道,在前面也提及到了ADC1和ADC2都有16個外部通道和2個內(nèi)部通道;而ADC3就有8個外部通道。外部通道對應(yīng)的是上圖中的ADCx_IN0、ADCx_IN1…ADCx_IN15。ADC1的通道16就是內(nèi)部通道,連接到芯片內(nèi)部的溫度傳感器,通道17連接到Vrefint。而ADC2的通道16和17連接到內(nèi)部的VSS。ADC3的通道9、14、15、16和17連接到的是內(nèi)部的VSS。具體的ADC通道表見表30.1.1所示:
表30.1.1 ADC通道表
③ 轉(zhuǎn)換順序
當(dāng)ADC的多個通道以任意順序進(jìn)行轉(zhuǎn)換就誕生了成組轉(zhuǎn)換,這里有兩種成組轉(zhuǎn)換類型:規(guī)則組和注入組。規(guī)則組就是圖中的規(guī)則通道,注入組就是圖中的注入通道。為了避免大家對輸入通道,以及規(guī)則通道和注入通道的理解混淆,后面規(guī)則通道以規(guī)則組來代稱,注入通道以注入組來代稱。
規(guī)則組最多允許16個輸入通道進(jìn)行轉(zhuǎn)換,而注入組最多允許4個輸入通道進(jìn)行轉(zhuǎn)換。這里講解一下規(guī)則組和注入組。
規(guī)則組(規(guī)則通道)
規(guī)則組,按字面理解,“規(guī)則”就是按照一定的順序,相當(dāng)于正常運行的程序,平常用到最多也是規(guī)則組。
注入組(注入通道)
注入組,按字面理解,“注入”就是打破原來的狀態(tài),相當(dāng)于中斷。當(dāng)程序執(zhí)行的時候,中斷是可以打斷程序的執(zhí)行。同這個類似,注入組轉(zhuǎn)換可以打斷規(guī)則組的轉(zhuǎn)換。假如在規(guī)則組轉(zhuǎn)換過程中,注入組啟動,那么注入組被轉(zhuǎn)換完成之后,規(guī)則組才得以繼續(xù)轉(zhuǎn)換。
為了便于理解,下面看一下規(guī)則組和注入組的執(zhí)行優(yōu)先級對比圖,如圖30.1.3所示:
圖30.1.3 規(guī)則組和注入組的執(zhí)行優(yōu)先級對比圖
了解了規(guī)則組和注入組的概念后,下面來看看它們的轉(zhuǎn)換順序,即轉(zhuǎn)換序列。轉(zhuǎn)換序列可以分為規(guī)則序列和注入序列。下面分別來介紹它們。
規(guī)則序列
規(guī)則組最多允許16個輸入通道進(jìn)行轉(zhuǎn)換,那么就需要設(shè)置通道轉(zhuǎn)換的順序,即規(guī)則序列。規(guī)則序列寄存器有3個,分別為SQR1、SQR2和SQR3。SQR3控制規(guī)則序列中的第1個到第6個轉(zhuǎn)換;SQR2控制規(guī)則序列中第7個到第12個轉(zhuǎn)換;SQR1控制規(guī)則序列中第13個到第16個轉(zhuǎn)換。規(guī)則序列寄存器控制關(guān)系匯總?cè)绫?0.1.2所示:
規(guī)則序列寄存器控制關(guān)系匯總
表30.1.2 規(guī)則序列寄存器控制關(guān)系匯總表
從上表可以知道,當(dāng)我們想設(shè)置ADC的某個輸入通道在規(guī)則序列的第1個轉(zhuǎn)換,只需要把相應(yīng)的輸入通道號的值寫入SQR3寄存器中的SQ1[4:0]位即可。例如想讓輸入通道5先進(jìn)行轉(zhuǎn)換,那么就可以把5這個數(shù)值寫入SQ1[4:0]位。如果還想讓輸入通道8在第2個轉(zhuǎn)換,那么就可以把8這個數(shù)值寫入SQ2[4:0]位。最后還要設(shè)置你的這個規(guī)則序列的輸入通道個數(shù),只需把輸入通道個數(shù)寫入SQR1的SQL[3:0]位。注意:寫入0到SQL[3:0]位,表示這個規(guī)則序列有1個輸入通道的意思,而不是0個輸入通道。
注入序列
注入序列,跟規(guī)則序列差不多,決定的是注入組的順序。注入組最大允許4個通道輸入,它的注入序列由JSQR寄存器配置。注入序列寄存器JSQR控制關(guān)系如表30.1.3所示:
注入序列有多少個輸入通道,只需要把輸入通道個數(shù)寫入到JL [ 1 : 0 ]位,范圍是0~3。注意:寫入0表示這個注入序列有一個輸入通道,而不是0個輸入通道。這個內(nèi)容很簡單。編程時容易犯錯的是注入序列的轉(zhuǎn)換順序問題,下面給大家講解一下。
如果JL[ 1 : 0 ]位的值小于3,即設(shè)置注入序列要轉(zhuǎn)換的通道個數(shù)小于4,則注入序列的轉(zhuǎn)換順序是從JSQx[ 4 : 0 ](x=4-JL[1:0])開始。例如:JL [ 1 : 0 ]=10、JSQ4 [ 4 : 0 ]= 00100、JSQ3 [ 4 : 0 ]= 00011、JSQ2 [ 4 : 0 ]= 00111、JSQ1 [ 4 : 0 ]= 00010,意味著這個注入序列的轉(zhuǎn)換順序是:7、3、4,而不是2、7、3。如果JL[ 1 : 0 ]=00,那么轉(zhuǎn)換順序是從JSQ4 [ 4 : 0 ]開始。
④ 觸發(fā)源
在配置好輸入通道以及轉(zhuǎn)換順序后,就可以進(jìn)行觸發(fā)轉(zhuǎn)換了。ADC的觸發(fā)轉(zhuǎn)換有兩種方法:分別是通過ADON位或外部事件觸發(fā)轉(zhuǎn)換。
(1)ADON位觸發(fā)轉(zhuǎn)換
當(dāng)ADC_CR2寄存器的ADON位為1時,再獨立給ADON位寫1(其它位不能一起改變,這是為了防止誤觸發(fā)),這時會啟動轉(zhuǎn)換。這種控制ADC啟動轉(zhuǎn)換的方式非常簡單。
(2)外部觸發(fā)轉(zhuǎn)換
另一種方法是通過外部事件觸發(fā)轉(zhuǎn)換,例如定時器捕獲、EXTI線和軟件觸發(fā),可以分為規(guī)則組外部觸發(fā)和注入組外部觸發(fā)。
規(guī)則組外部觸發(fā)使用方法是將EXTTRIG位置1,并且通過EXTSET[2:0]位選擇規(guī)則組啟動轉(zhuǎn)換的觸發(fā)源。如果EXTSET[2:0]位設(shè)置為111,那么可以通過SWSTART為啟動ADC轉(zhuǎn)換,相當(dāng)于軟件觸發(fā)。
注入組外部觸發(fā)使用方法是將JEXTTRIG位置1,并且通過JEXTSET[2:0]位選擇注入組啟動轉(zhuǎn)換的觸發(fā)源。如果JEXTSET[2:0]位設(shè)置為111,那么可以通過JSWSTART為啟動ADC轉(zhuǎn)換,相當(dāng)于軟件觸發(fā)。
ADC1和ADC2的觸發(fā)源是一樣的,ADC3的觸發(fā)源和ADC1/2有所不同,這個需要注意。
⑤ 轉(zhuǎn)換時間
(1)ADC時鐘
在學(xué)習(xí)轉(zhuǎn)換時間之前,我們先來了解ADC時鐘。從標(biāo)號⑤框出來部分可以看到ADC時鐘是要經(jīng)過ADC預(yù)分頻器的,那么ADC的時鐘源是什么?ADC預(yù)分頻器的分頻系數(shù)可以設(shè)置的范圍又是多少?以及ADC時鐘頻率的最大值又是多少?下面將為大家解答。
ADC的輸入時鐘是由PCLK2經(jīng)過分頻產(chǎn)生的,分頻系數(shù)是由RCC_CFGR寄存器的ADCPRE[1:0]位設(shè)置的,可選擇2/4/8/16分頻。需要注意的是,ADC的輸入時鐘頻率最大值是14MHz,如果超過這個值將會導(dǎo)致ADC的轉(zhuǎn)換結(jié)果準(zhǔn)確度下降。
一般我們設(shè)置PCLK2為72MHz。為了不超過ADC的最大輸入時鐘頻率14MHz,我們設(shè)置ADC的預(yù)分頻器分頻系數(shù)為6,就可以得到ADC的輸入時鐘頻率為72MHz/6,即12MHz。例程中,我們也是如此設(shè)置的。
(2)轉(zhuǎn)換時間
STM32F103的ADC總轉(zhuǎn)換時間的計算公式如下:
TCONV = 采樣時間 + 12.5個周期
采樣時間可通過ADC_SMPR1和ADC_SMPR2寄存器中的SMPx[2:0]位設(shè)置,x=017。ADC_SMPR1控制的是通道09,ADC_SMPR2控制的是通道10~17。每個輸入通道都支持通過編程來選擇不同的采樣時間,采樣時間可選的范圍如下:
?SMP = 000:1.5個ADC時鐘周期
?SMP = 001:7.5個ADC時鐘周期
?SMP = 010:13.5個ADC時鐘周期
?SMP = 011:28.5個ADC時鐘周期
?SMP = 100:41.5個ADC時鐘周期
?SMP = 101:55.5個ADC時鐘周期
?SMP = 110:71.5個ADC時鐘周期
?SMP = 111:239.5個ADC時鐘周期
可以看出,采樣時間最小是1.5個時鐘周期,設(shè)置為這個值,那么我們可以得到最短的轉(zhuǎn)換時間。下面以我們例程的ADC時鐘配置為例,來給大家計算一下ADC的最短轉(zhuǎn)換時間,計算過程如下:
TCONV = 1.5個ADC時鐘周期 + 12.5個ADC時鐘周期 = 14個ADC時鐘周期
例程中,PCLK2的時鐘是72MHz,經(jīng)過ADC時鐘預(yù)分頻器的6分頻后,ADC時鐘頻率為12MHz。代入上式可得到:
TCONV = 14個ADC時鐘周期 = = 1.17us
⑥數(shù)據(jù)寄存器
ADC轉(zhuǎn)換完成后的數(shù)據(jù)輸出寄存器。根據(jù)轉(zhuǎn)換組的不同,規(guī)則組的完成轉(zhuǎn)換的數(shù)據(jù)輸出到ADC_DR寄存器,注入組的完成轉(zhuǎn)換的數(shù)據(jù)輸出到ADC_JDRx寄存器。假如是使用雙重模式,規(guī)則組的數(shù)據(jù)也是存放在ADC_DR寄存器。下面給大家簡單介紹一下這兩個寄存器。
(1)ADC規(guī)則數(shù)據(jù)寄存器(ADC_DR)
ADC規(guī)則組數(shù)據(jù)寄存器ADC_DR是一個32位的寄存器,獨立模式時只使用到該寄存器低16位保存ADC1/2/3的規(guī)則轉(zhuǎn)換數(shù)據(jù)。在雙ADC模式下,高16位用于保存ADC2轉(zhuǎn)換的數(shù)據(jù),低16位用于保存ADC1轉(zhuǎn)換的數(shù)據(jù)。
因為ADC的精度是12位的,ADC_DR寄存器無論高16位還是低16,存放數(shù)據(jù)的位寬都是16位的,所以允許選擇數(shù)據(jù)對齊方式。由ADC_CR2寄存器的ALIGN位設(shè)置數(shù)據(jù)對齊方式,可選擇:右對齊或者左對齊。
細(xì)心的朋友可能發(fā)現(xiàn),規(guī)則組最多有16個輸入通道,而ADC規(guī)則數(shù)據(jù)寄存器只有一個,如果一個規(guī)則組用到好幾個通道,數(shù)據(jù)怎么讀取?如果使用多通道轉(zhuǎn)換,那么這些通道的數(shù)據(jù)也會存放在DR里面,按照規(guī)則組的順序,上一個通道轉(zhuǎn)換的數(shù)據(jù),會被下一個通道轉(zhuǎn)換的數(shù)據(jù)覆蓋掉,所以當(dāng)通道轉(zhuǎn)換完成后要及時把數(shù)據(jù)取走。比較常用的方法是使用DMA模式。當(dāng)規(guī)則組的通道轉(zhuǎn)換結(jié)束時,就會產(chǎn)生DMA請求,這樣就可以及時把轉(zhuǎn)換的數(shù)據(jù)搬運到用戶指定的目的地址存放。注意:只有ADC1和ADC3可以產(chǎn)生DAM請求,而由ADC2轉(zhuǎn)換的數(shù)據(jù)可以通過雙ADC模式,利用ADC1的DMA功能傳輸。
(2)ADC注入數(shù)據(jù)寄存器x(ADC_JDRx)(x=1~4)
ADC注入數(shù)據(jù)寄存器有4個,注入組最多有4個輸入通道,剛好每個通道都有自己對應(yīng)的數(shù)據(jù)寄存器。ADC_JDRx寄存器是32位的,低16位有效,高16位保留,數(shù)據(jù)也同樣需要選擇對齊方式。也是由ADC_CR2寄存器的ALIGN位設(shè)置數(shù)據(jù)對齊方式,可選擇:右對齊或者左對齊。
⑦中斷
ADC中斷可分為三種:規(guī)則組轉(zhuǎn)換結(jié)束中斷、注入組轉(zhuǎn)換結(jié)束中斷、設(shè)置了模擬看門狗狀態(tài)位中斷。它們都有獨立的中斷使能位,分別由ADC_CR寄存器的EOCIE、JEOCIE、AWDIE位設(shè)置,對應(yīng)的標(biāo)志位分別是EOC、JEOC、AWD。
模擬看門狗中斷
模擬看門狗中斷發(fā)生條件:首先通過ADC_LTR和ADC_HTR寄存器設(shè)置低閾值和高閾值,然后開啟了模擬看門狗中斷后,當(dāng)被ADC轉(zhuǎn)換的模擬電壓低于低閾值或者高于高閾值時,就會產(chǎn)生中斷。例如我們設(shè)置高閾值是3.0V,那么模擬電壓超過3.0V的時候,就會產(chǎn)生模擬看門狗中斷,低閾值的情況類似。
DMA請求
規(guī)則組和注入組的轉(zhuǎn)換結(jié)束后,除了可以產(chǎn)生中斷外,還可以產(chǎn)生DMA請求,我們利用DMA及時把轉(zhuǎn)換好的數(shù)據(jù)傳輸?shù)街付ǖ膬?nèi)存里,防止數(shù)據(jù)被覆蓋。
注意:只有ADC1和ADC3可以產(chǎn)生DAM請求,DMA相關(guān)的知識請回顧DMA實驗。
⑧單次轉(zhuǎn)換模式和連續(xù)轉(zhuǎn)換模式
單次轉(zhuǎn)換模式和連續(xù)轉(zhuǎn)換模式在框圖中是沒有標(biāo)號,為了更好地學(xué)習(xí)后續(xù)的內(nèi)容,這里簡單給大家講講。
(1)單次轉(zhuǎn)換模式
通過將ADC_CR2寄存器的CONT位置0選擇單次轉(zhuǎn)換模式。該模式下,ADC只執(zhí)行一次轉(zhuǎn)換,由ADC_CR2寄存器的ADON位啟動(只適用于規(guī)則組),也可以通過外部觸發(fā)啟動(適用于規(guī)則組或注入組)。
如果規(guī)則組的一個輸入通道被轉(zhuǎn)換,那么轉(zhuǎn)換的數(shù)據(jù)被儲存在16位ADC_DR寄存器中、EOC(轉(zhuǎn)換結(jié)束)標(biāo)志位被置1、如果設(shè)置了EOCIE位,則產(chǎn)生中斷,然后ADC停止。
如果注入組的一個輸入通道被轉(zhuǎn)換,那么轉(zhuǎn)換的數(shù)據(jù)被儲存在16位ADC_DRJx寄存器中、JEOC(轉(zhuǎn)換結(jié)束)標(biāo)志位被置1、如果設(shè)置了JEOCIE位,則產(chǎn)生中斷,然后ADC停止。
(2)連續(xù)轉(zhuǎn)換模式
通過將ADC_CR2寄存器的CONT位置1選擇連續(xù)轉(zhuǎn)換模式。該模式下,ADC完成上一個通道的轉(zhuǎn)換后會馬上自動地啟動下一個通道的轉(zhuǎn)換,由ADC_CR2寄存器的ADON位啟動,也可以通過外部觸發(fā)啟動。
如果規(guī)則組的一個輸入通道被轉(zhuǎn)換,那么轉(zhuǎn)換的數(shù)據(jù)被儲存在16位ADC_DR寄存器中、EOC(轉(zhuǎn)換結(jié)束)標(biāo)志位被置1、如果設(shè)置了EOCIE位,則產(chǎn)生中斷。
如果注入組的一個輸入通道被轉(zhuǎn)換,那么轉(zhuǎn)換的數(shù)據(jù)被儲存在16位ADC_DRJx寄存器中、JEOC(轉(zhuǎn)換結(jié)束)標(biāo)志位被置1、如果設(shè)置了JEOCIE位,則產(chǎn)生中斷。
⑨掃描模式
掃描模式在框圖中是沒有標(biāo)號,為了更好地學(xué)習(xí)后續(xù)的內(nèi)容,這里簡單給大家講講。
可以通過ADC_CR1寄存器的SCAN位配置是否使用掃描模式。如果選擇掃描模式,ADC會掃描所有被ADC_SQRx寄存器或ADC_JSQR選中的所有通道,并對規(guī)則組或者注入組的每個通道執(zhí)行單次轉(zhuǎn)換,然后停止轉(zhuǎn)換。但如果還設(shè)置了CONT位,即選擇連續(xù)轉(zhuǎn)換模式,那么轉(zhuǎn)換不會在選擇組的最后一個通道上停止,而是再次從選擇組的第一個通道繼續(xù)轉(zhuǎn)換。
如果設(shè)置了DMA位,在每次EOC后, DMA控制器把規(guī)則組通道的轉(zhuǎn)換數(shù)據(jù)傳輸?shù)絊RAM中。而注入通道轉(zhuǎn)換的數(shù)據(jù)總是存儲在ADC_JDRx寄存器中。
到這里,我們基本上介紹了ADC的大多數(shù)基礎(chǔ)的知識點,其它知識后面用到會繼續(xù)補充,如果還有不懂的內(nèi)容,請參考《STM32F10xxx參考手冊_V10(中文版).pdf》的第11章。
30.2 單通道ADC采集實驗
本實驗我們來學(xué)習(xí)單通道ADC采集實驗。本實驗使用規(guī)則組單通道的單次轉(zhuǎn)換模式,并且通過軟件觸發(fā),即由ADC_CR2寄存器的SWSTART位啟動。下面先帶大家來了解本實驗要配置的寄存器。
30.2.1 ADC寄存器
這里,我們只介紹本實驗用到的寄存器的關(guān)鍵位,其它寄存器后續(xù)用到會繼續(xù)介紹。
? ADC控制寄存器1(ADC_CR1)
ADC控制寄存器1描述如圖30.2.1.1所示:
圖30.2.1.1 ADC_CR1寄存器
SCAN位用于選擇是否使用掃描模式。本實驗我們使用單通道采集,所以沒必要選擇掃描模式,該位置0即可。
DUALMOD[3:0]位用來設(shè)置ADC的操作模式,我們的例程中ADC相關(guān)的實驗都是使用獨立模式,所以設(shè)置為0000即可。
? ADC控制寄存器2(ADC_CR2)
ADC控制寄存器2描述如圖30.2.1.2所示:
圖30.2.1.2 ADC_CR2存器
該寄存器我們針對性的介紹一些位:ADON位用于打開或關(guān)閉AD轉(zhuǎn)換器,還可以用于觸發(fā)ADC轉(zhuǎn)換。CONT位用于設(shè)置單次轉(zhuǎn)換模式還是連續(xù)轉(zhuǎn)換模式,本實驗我們使用單次轉(zhuǎn)換模式,所以CONT位置0即可。CAL位用于開啟AD校準(zhǔn)。RSTCAL位用于判斷校準(zhǔn)寄存器是否已初始化。ALIGN用于設(shè)置數(shù)據(jù)對齊,我們使用右對齊,所以該位置0。EXTSEL[2:0]位用于選擇規(guī)則組啟動轉(zhuǎn)換的外部事件觸發(fā)源,本實驗使用的是軟件觸發(fā)(SWSTART),所以這三個位置為111。EXTTRIG位必須置1,EXTSEL[2:0]位才能選擇軟件觸發(fā)(SWSTART),別漏了這步,否則設(shè)置軟件觸發(fā)會不成功。SWSTART位用于啟動一次規(guī)則組通道的轉(zhuǎn)換,即軟件觸發(fā)轉(zhuǎn)換。
? ADC采樣事件寄存器1(ADC_SMPR1)
ADC采樣事件寄存器1描述如圖30.2.1.3所示:
圖30.2.1.3 ADC_SMPR1寄存器
? ADC采樣事件寄存器2(ADC_SMPR2)
ADC采樣事件寄存器2描述如圖30.2.1.4所示:
圖30.2.1.4 ADC_SMPR2寄存器
ADC采樣時間設(shè)置需要由兩個寄存器設(shè)置,ADC_SMPR1和ADC_SMPR1,分別設(shè)置通道1017和通道09的采樣時間,每個通道用3個位設(shè)置??梢钥闯鯝DC的每個通道的采樣時間是支持單獨設(shè)置的。
一般每個要轉(zhuǎn)換的通道,采樣時間建議盡量長一點,以獲得較高的準(zhǔn)確度,但是這樣會降低ADC的轉(zhuǎn)換速率,看大家怎么衡量選擇了。本實驗中,我們設(shè)置采樣時間是239.5個周期。結(jié)合前面介紹過的轉(zhuǎn)換時間公式:
TCONV = 采樣時間 + 12.5個周期
以及例程中,PCLK2的時鐘是72MHz,經(jīng)過ADC時鐘預(yù)分頻器的6分頻后,ADC時鐘頻率為12MHz。代入上式可得到:
TCONV = 239.5 + 12.5個ADC時鐘周期 = = 21us
由上式可得,ADC的轉(zhuǎn)換時間是21us。
? ADC規(guī)則序列寄存器1
ADC規(guī)則序列寄存器共有3個,這幾個寄存器的功能都差不多,這里我們僅介紹一下ADC規(guī)則序列寄存器1(ADC_SQR1),描述如圖30.2.1.5所示:
圖30.2.1.5 ADC_SQR1寄存器
L[3:0]用于設(shè)置規(guī)則組序列的長度,取值范圍:015,表示規(guī)則組的長度是116。本實驗只用了1個輸入通道,所以L[3:0]位設(shè)置為0000即可。
SQ13[4:0]SQ16[4:0]位設(shè)置規(guī)則組序列的第1316個轉(zhuǎn)換編號,第1~12個轉(zhuǎn)換編號的設(shè)置請查看ADC_SQR2和ADC_SQR3寄存器。設(shè)置過程非常簡單,忘記了請參考前面給大家整理出來的規(guī)則序列寄存器控制關(guān)系匯總表。
本實驗我們使用單通道,ADC1通道14,所以規(guī)則組序列里只有一個輸入通道,我們將ADC_SQR3寄存器的SQ1[4:0]位的值設(shè)置為14即可。
? ADC規(guī)則數(shù)據(jù)寄存器(ADC_DR)
ADC規(guī)則數(shù)據(jù)寄存器描述如圖30.2.1.6所示:
圖30.2.1.6 ADC_DR寄存器
在規(guī)則序列中AD轉(zhuǎn)換結(jié)果都將被存在這個寄存器里面,而注入通道的轉(zhuǎn)換結(jié)果被保存在ADC_JDRx里面。該寄存器的數(shù)據(jù)可以通過ADC_CR2的ALIGN位設(shè)置左對齊還是右對齊。在讀取數(shù)據(jù)的時候要注意。
? ADC狀態(tài)寄存器(ADC_SR)
ADC狀態(tài)寄存器描述如圖30.2.1.7所示:
圖30.2.1.7 ADC_SR寄存器
該寄存器保存了ADC轉(zhuǎn)換時的各種狀態(tài)。本實驗我們通過EOC位的狀態(tài)來判斷ADC轉(zhuǎn)換是否完成,如果查詢到EOC位被硬件置1,就可以從ADC_DR寄存器中讀取轉(zhuǎn)換結(jié)果,否則需要等待轉(zhuǎn)換完成。
至此,本實驗要用到的ADC關(guān)鍵寄存器全部介紹完了,對于未介紹的部分,請大家參考《STM32F10xxx參考手冊_V10(中文版).pdf》第11章相關(guān)內(nèi)容。
30.2.2 硬件設(shè)計
- 例程功能
采集ADC1通道1(PA1)上面的電壓,并在LCD模塊上面顯示ADC規(guī)則數(shù)據(jù)寄存器12位的轉(zhuǎn)換值以及將該值換算成電壓后的電壓值。使用杜邦線將ADC和RV1排針連接,使得PA1連接到電位器上,然后將ADC采集到的數(shù)據(jù)和轉(zhuǎn)換后的電壓值在TFTLCD屏中顯示。用戶可以通過調(diào)節(jié)電位器的旋鈕改變電壓值。LED0閃爍,提示程序運行。 - 硬件資源
1)LED燈
LED0 – PB5
2)串口1(PA9/PA10連接在板載USB轉(zhuǎn)串口芯片CH340上面)
3)正點原子 2.8/3.5/4.3/7/10寸TFTLCD模塊(僅限MCU屏,16位8080并口驅(qū)動)
4)ADC1 :
通道1 – PA1 - 原理圖
ADC屬于STM32F103內(nèi)部資源,實際上我們只需要軟件設(shè)置就可以正常工作,另外還需要將待測量的電壓源連接到ADC通道上,以便ADC測量。本實驗,我們通過ADC1的通道1(PA1)來采集外部電壓值,開發(fā)板有一個電位器,可調(diào)節(jié)的電壓范圍是:0~3.3V。我們可以通過杜邦線將PA1與電位器連接,如下圖所示:
圖30.2.2.1 PA1(對應(yīng)ADC排針)與電位器示意圖
使用杜邦線將ADC和RV1排針連接好后,并下載程序后,就可以用螺絲刀調(diào)節(jié)電位器變換多種電壓值進(jìn)行測量。
有的朋友可能還想測量其它地方的電壓值,我們只需要1根杜邦線,一端接到ADC排針上,另外一端就接你要測試的電壓點。一定要保證測試點的電壓在0~3.3V的電壓范圍,否則可能燒壞我們的ADC,甚至是整個主控芯片。
30.2.3 程序設(shè)計
30.2.3.1 ADC的HAL庫驅(qū)動
ADC在HAL庫中的驅(qū)動代碼在stm32f1xx_hal_adc.c和stm32f1xx_hal_adc_ex.c文件(及其頭文件)中。
- HAL_ADC_Init函數(shù)
ADC的初始化函數(shù),其聲明如下:
HAL_StatusTypeDef HAL_ADC_Init(ADC_HandleTypeDef *hadc);
?函數(shù)描述:
用于初始化ADC。
?函數(shù)形參:
形參1是ADC_HandleTypeDef結(jié)構(gòu)體類型指針變量,其定義如下:
typedef struct
{
ADC_TypeDef *Instance; /* ADC寄存器基地址 */
ADC_InitTypeDef Init; /* ADC參數(shù)初始化結(jié)構(gòu)體變量 */
DMA_HandleTypeDef *DMA_Handle; /* DMA配置結(jié)構(gòu)體 */
HAL_LockTypeDef Lock; /* ADC鎖定對象 */
__IO uint32_t State; /* ADC工作狀態(tài) */
__IO uint32_t ErrorCode; /* ADC錯誤代碼 */
}ADC_HandleTypeDef;
該結(jié)構(gòu)體定義和其他外設(shè)比較類似,我們著重看第二個成員變量Init含義,它是結(jié)構(gòu)體ADC_InitTypeDef類型,結(jié)構(gòu)體ADC_InitTypeDef定義為:
typedef struct {
uint32_t DataAlign; /* 設(shè)置數(shù)據(jù)的對齊方式 */
uint32_t ScanConvMode; /* 掃描模式 */
FunctionalState ContinuousConvMode; /* 開啟連續(xù)轉(zhuǎn)換模式否則就是單次轉(zhuǎn)換模式 */
uint32_t NbrOfConversion; /* 設(shè)置轉(zhuǎn)換通道數(shù)目 */
FunctionalState DiscontinuousConvMode; /* 是否使用規(guī)則通道組間斷模式 */
uint32_t NbrOfDiscConversion; /* 配置間斷模式的規(guī)則通道個數(shù) */
uint32_t ExternalTrigConv; /* ADC外部觸發(fā)源選擇 */
} ADC_InitTypeDef;
- DataAlign:用于設(shè)置數(shù)據(jù)的對齊方式,這里可以選擇右對齊或者是左對齊,該參數(shù)可選為:ADC_DATAALIGN_RIGHT和ADC_DATAALIGN_LEFT。
- ScanConvMode:配置是否使用掃描。如果是單通道轉(zhuǎn)換使用ADC_SCAN_DISABLE,如果是多通道轉(zhuǎn)換使用ADC_SCAN_ENABLE。
- ContinuousConvMode:可選參數(shù)為ENABLE和DISABLE,配置自動連續(xù)轉(zhuǎn)換還是單次轉(zhuǎn)換。使用ENABLE配置為使能自動連續(xù)轉(zhuǎn)換;使用DISABLE配置為單次轉(zhuǎn)換,轉(zhuǎn)換一次后停止需要手動控制才重新啟動轉(zhuǎn)換。
- NbrOfConversion:指定規(guī)則組轉(zhuǎn)換通道數(shù)目,范圍是:1~16。
- DiscontinuousConvMode:配置是否使用規(guī)則通道組間斷模式,比如要轉(zhuǎn)換的通道有1、2、5、7、8、9,那么第一次觸發(fā)會進(jìn)行通道1和2,下次觸發(fā)就是轉(zhuǎn)換通道5和7,這樣不連續(xù)的轉(zhuǎn)換,依次類推。此參數(shù)只有將ScanConvMode使能,還有ContinuousConvMode失能的情況下才有效,不可同時使能。
- NbrOfDiscConversion:配置間斷模式的通道個數(shù),禁止規(guī)則通道組間斷模式后,此參數(shù)忽略。
- ExternalTrigConv:外部觸發(fā)方式的選擇,如果使用軟件觸發(fā),那么外部觸發(fā)會關(guān)閉。
?函數(shù)返回值:
HAL_StatusTypeDef枚舉類型的值。
- HAL_ADCEx_Calibration_Start函數(shù)
ADC的自校準(zhǔn)函數(shù),其聲明如下:
HAL_StatusTypeDef HAL_ADCEx_Calibration_Start(ADC_HandleTypeDef *hadc);
?函數(shù)描述:
首先調(diào)用HAL_ADC_Init函數(shù)配置了相關(guān)的功能后,再調(diào)用此函數(shù)進(jìn)行ADC自校準(zhǔn)功能。
?函數(shù)形參:
ADC_HandleTypeDef結(jié)構(gòu)體類型指針變量。
?函數(shù)返回值:
HAL_StatusTypeDef枚舉類型的值。 - HAL_ADC_ConfigChannel函數(shù)
ADC通道配置函數(shù),其聲明如下:
HAL_StatusTypeDef HAL_ADC_ConfigChannel(ADC_HandleTypeDef *hadc,
ADC_ChannelConfTypeDef *sConfig);
?函數(shù)描述:
調(diào)用了HAL_ADC_Init函數(shù)配置了相關(guān)的功能后,就可以調(diào)用此函數(shù)配置ADC具體通道。
?函數(shù)形參:
形參1是ADC_HandleTypeDef結(jié)構(gòu)體類型指針變量。
形參2是ADC_ChannelConfTypeDef結(jié)構(gòu)體類型指針變量,用于配置ADC采樣時間,使用的通道號,單端或者差分方式的配置等。該結(jié)構(gòu)體定義如下:
typedef struct {
uint32_t Channel; /* ADC轉(zhuǎn)換通道*/
uint32_t Rank; /* ADC轉(zhuǎn)換順序 */
uint32_t SamplingTime; /* ADC采樣周期 */
} ADC_ChannelConfTypeDef;
- Channel:ADC轉(zhuǎn)換通道,范圍:0~19。
- Rank:在常規(guī)轉(zhuǎn)換中的常規(guī)組的轉(zhuǎn)換順序,可以選擇1~16。
- SamplingTime:ADC的采樣周期,最大810.5個ADC時鐘周期,要求盡量大以減少誤差。
?函數(shù)返回值:
HAL_StatusTypeDef枚舉類型的值。
- HAL_ADC_Start函數(shù)
ADC轉(zhuǎn)換啟動函數(shù),其聲明如下:
HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc);
?函數(shù)描述:
當(dāng)配置好ADC的基礎(chǔ)的功能后,就調(diào)用此函數(shù)啟動ADC。
?函數(shù)形參:
ADC_HandleTypeDef結(jié)構(gòu)體類型指針變量。
?函數(shù)返回值:
HAL_StatusTypeDef枚舉類型的值。 - HAL_ADC_PollForConversion函數(shù)
等待ADC規(guī)則組轉(zhuǎn)換完成函數(shù),其聲明如下:
HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef *hadc,
uint32_t Timeout);
?函數(shù)描述:
一般先調(diào)用HAL_ADC_Start函數(shù)啟動轉(zhuǎn)換,再調(diào)用該函數(shù)等待轉(zhuǎn)換完成,然后再調(diào)用HAL_ADC_GetValue函數(shù)來獲取當(dāng)前的轉(zhuǎn)換值。
?函數(shù)形參:
形參1是ADC_HandleTypeDef結(jié)構(gòu)體類型指針變量。
形參2是等待轉(zhuǎn)換的等待時間,單位是毫秒(ms)。
?函數(shù)返回值:
HAL_StatusTypeDef枚舉類型的值。 - HAL_ADC_GetValue函數(shù)
獲取常規(guī)組ADC轉(zhuǎn)換值函數(shù),其聲明如下:
uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef hadc);
?函數(shù)描述:
一般先調(diào)用HAL_ADC_Start函數(shù)啟動轉(zhuǎn)換,再調(diào)用HAL_ADC_PollForConversion函數(shù)等待轉(zhuǎn)換完成,然后再調(diào)用HAL_ADC_GetValue函數(shù)來獲取當(dāng)前的轉(zhuǎn)換值。
?函數(shù)形參:
形參1是ADC_HandleTypeDef結(jié)構(gòu)體類型指針變量。
?函數(shù)返回值:
當(dāng)前的轉(zhuǎn)換值,uint32_t類型數(shù)據(jù)。
單通道ADC采集配置步驟
1)開啟ADCx和ADC通道對應(yīng)的IO時鐘,并配置該IO為模擬功能
首先開啟ADCx的時鐘,然后配置GPIO為模擬模式。本實驗我們默認(rèn)用到ADC1通道1,對應(yīng)IO是PA1,它們的時鐘開啟方法如下:
__HAL_RCC_ADC1_CLK_ENABLE(); / 使能ADC1時鐘 /
__HAL_RCC_GPIOA_CLK_ENABLE(); / 開啟GPIOA時鐘 */
2)初始化ADCx, 配置其工作參數(shù)
通過HAL_ADC_Init函數(shù)來設(shè)置ADCx時鐘分頻系數(shù)、分辨率、模式、掃描方式、對齊方式等信息。
注意:該函數(shù)會調(diào)用:HAL_ADC_MspInit回調(diào)函數(shù)來存放ADC及GPIO時鐘使能、GPIO初始化等代碼。
3)配置ADC通道并啟動AD轉(zhuǎn)換器
在HAL庫中,通過HAL_ADC_ConfigChannel函數(shù)來選擇要配置ADC的通道,并設(shè)置規(guī)則序列、采樣時間等。
配置好ADC通道之后,通過HAL_ADC_Start函數(shù)啟動AD轉(zhuǎn)換器。
4)讀取ADC值
這里選擇查詢方式讀取,在讀取ADC值之前需要調(diào)用HAL_ADC_PollForConversion等待上一次轉(zhuǎn)換結(jié)束。然后就可以通過HAL_ADC_GetValue來讀取ADC值。
30.2.3.2 程序流程圖
圖30.2.3.2.1 單通道ADC采集實驗程序流程圖
30.2.3.3 程序解析
這里我們只講解核心代碼,詳細(xì)的源碼請大家參考光盤本實驗對應(yīng)源碼。ADC驅(qū)動源碼包括兩個文件:adc.c和adc.h。本章有四個實驗,每一個實驗的代碼都是在上一個實驗后面追加。
adc.h文件針對ADC及通道引腳定義了一些宏定義,具體如下:
/* ADC及引腳 定義 */
#define ADC_ADCX_CHY_GPIO_PORT GPIOA
#define ADC_ADCX_CHY_GPIO_PIN GPIO_PIN_1
#define ADC_ADCX_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE();\
}while(0) /* PA口時鐘使能 */
#define ADC_ADCX ADC1
#define ADC_ADCX_CHY ADC_CHANNEL_1 /* 通道Y, 0 <= Y <= 16 */
/* ADC1時鐘使能 */
#define ADC_ADCX_CHY_CLK_ENABLE() do{ __HAL_RCC_ADC1_CLK_ENABLE();}while(0)
ADC的通道與引腳的對應(yīng)關(guān)系在《STM32F103ZET6.pdf》數(shù)據(jù)手冊可以查到,我們這里使用ADC1的通道1,在數(shù)據(jù)手冊中的表格為:
表30.2.3.3.1 ADC通道1對應(yīng)引腳查看表
下面直接開始介紹adc.c的程序,首先是ADC初始化函數(shù)。
/**
* @brief ADC初始化函數(shù)
* @note 本函數(shù)支持ADC1/ADC2任意通道, 但是不支持ADC3
* 我們使用12位精度, ADC采樣時鐘=12M, 轉(zhuǎn)換時間為: 采樣周期 + 12.5個ADC周期
* 設(shè)置最大采樣周期: 239.5, 則轉(zhuǎn)換時間 = 252 個ADC周期 = 21us
* @param 無
* @retval 無
*/
void adc_init(void)
{
g_adc_handle.Instance = ADC_ADCX; /* 選擇哪個ADC */
g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 數(shù)據(jù)對齊方式:右對齊 */
g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE; /* 非掃描模式 */
g_adc_handle.Init.ContinuousConvMode = DISABLE; /* 關(guān)閉連續(xù)轉(zhuǎn)換模式 */
g_adc_handle.Init.NbrOfConversion = 1; /* 范圍是1~16,這里用到1個規(guī)則通道 */
g_adc_handle.Init.DiscontinuousConvMode = DISABLE; /* 禁止規(guī)則通道組間斷模式 */
/* 配置間斷模式的規(guī)則通道個數(shù),禁止規(guī)則通道組間斷模式后,此參數(shù)忽略 */
g_adc_handle.Init.NbrOfDiscConversion = 0;
g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 軟件觸發(fā) */
HAL_ADC_Init(&g_adc_handle); /* 初始化 */
HAL_ADCEx_Calibration_Start(&g_adc_handle); /* 校準(zhǔn)ADC */
}
該函數(shù)主要調(diào)用了兩個HAL庫函數(shù),HAL_ADC_Init函數(shù)配置了選擇哪個ADC、數(shù)據(jù)對齊方式、是否使用掃描模式等參數(shù),HAL_ADCEx_Calibration_Start函數(shù)用于校準(zhǔn)ADC。另外HAL_ADC_Init函數(shù)會調(diào)用它的MSP回調(diào)函數(shù)HAL_ADC_MspInit,該函數(shù)用來存放使能ADC和通道對應(yīng)IO的時鐘和初始化IO口等代碼,其定義如下:
/**
* @brief ADC底層驅(qū)動,引腳配置,時鐘使能
此函數(shù)會被HAL_ADC_Init()調(diào)用
* @param hadc:ADC句柄
* @retval 無
*/
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
{
if(hadc->Instance == ADC_ADCX)
{
GPIO_InitTypeDef gpio_init_struct;
RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
ADC_ADCX_CHY_CLK_ENABLE(); /* 使能ADCx時鐘 */
ADC_ADCX_CHY_GPIO_CLK_ENABLE(); /* 開啟GPIO時鐘 */
/* 設(shè)置ADC時鐘 */
adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;/* ADC外設(shè)時鐘 */
/* 分頻系數(shù)為6,所以ADC的時鐘為72M/6=12MHz */
adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;
HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); /* 設(shè)置ADC時鐘 */
/* 設(shè)置AD采集通道對應(yīng)IO引腳工作模式 */
gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN; /* ADC通道IO引腳 */
gpio_init_struct.Mode = GPIO_MODE_ANALOG; /* 模擬 */
HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);
}
}
可以看到在HAL_ADC_MspInit函數(shù)中,我們除了使能ADC和通道對應(yīng)IO時鐘、初始化IO外,還配置了ADC的時鐘預(yù)分頻系數(shù)。ADC的時鐘源是PCLK2(72MHz),經(jīng)過6分頻后,得到ADC的輸入時鐘是12MHz。
接下來要介紹的函數(shù)是adc_channel_set,其定義如下:
/**
* @brief 設(shè)置ADC通道采樣時間
* @param adcx : adc句柄指針,ADC_HandleTypeDef
* @param ch : 通道號, ADC_CHANNEL_0~ADC_CHANNEL_17
* @param stime: 采樣時間 0~7, 對應(yīng)關(guān)系為:
* @arg ADC_SAMPLETIME_1CYCLE_5, 1.5個ADC時鐘周
ADC_SAMPLETIME_7CYCLES_5, 7.5個ADC時鐘周期
* @arg ADC_SAMPLETIME_13CYCLES_5, 13.5個ADC時鐘周期 ADC_SAMPLETIME_28CYCLES_5, 28.5個ADC時鐘周期
* @arg ADC_SAMPLETIME_41CYCLES_5, 41.5個ADC時鐘周期 ADC_SAMPLETIME_55CYCLES_5, 55.5個ADC時鐘周期
* @arg ADC_SAMPLETIME_71CYCLES_5, 71.5個ADC時鐘周期 ADC_SAMPLETIME_239CYCLES_5, 239.5個ADC時鐘周期
* @param rank: 多通道采集時需要設(shè)置的采集編號,
假設(shè)你定義channle1的rank=1,channle2 的rank=2,
那么對應(yīng)你在DMA緩存空間的變量數(shù)組AdcDMA[0] 就i是channle1的轉(zhuǎn)換結(jié)果,AdcDMA[1]就是通道2的轉(zhuǎn)換結(jié)果。
單通道DMA設(shè)置為 ADC_REGULAR_RANK_1
* @arg 編號1~16:ADC_REGULAR_RANK_1~ADC_REGULAR_RANK_16
* @retval 無
*/
void adc_channel_set(ADC_HandleTypeDef *adc_handle, uint32_t ch,uint32_t rank, uint32_t stime)
{
ADC_ChannelConfTypeDef adc_ch_conf;
adc_ch_conf.Channel = ch; /* 通道 */
adc_ch_conf.Rank = rank; /* 序列 */
adc_ch_conf.SamplingTime = stime; /* 采樣時間 */
HAL_ADC_ConfigChannel(adc_handle, &adc_ch_conf); /* 通道配置 */
}
該函數(shù)主要是通過HAL_ADC_ConfigChannel函數(shù)選擇要配置的ADC規(guī)則組通道,并設(shè)置通道的序列號和采樣時間。
下面要介紹的是獲得ADC轉(zhuǎn)換后的結(jié)果函數(shù),其定義如下:
/**
* @brief 獲得ADC轉(zhuǎn)換后的結(jié)果
* @param ch: 通道值 0~17,取值范圍為:ADC_CHANNEL_0~ADC_CHANNEL_17
* @retval 無
*/
uint32_t adc_get_result(uint32_t ch)
{
adc_channel_set(&g_adc_handle , ch, ADC_REGULAR_RANK_1, ADC_SAMPLETIME_239CYCLES_5); /* 設(shè)置通道/序列和采樣時間 */
HAL_ADC_Start(&g_adc_handle); /* 開啟ADC */
HAL_ADC_PollForConversion(&g_adc_handle, 10); /* 等待轉(zhuǎn)換結(jié)束 */
/* 返回最近一次ADC1規(guī)則組的轉(zhuǎn)換結(jié)果 */
return (uint16_t)HAL_ADC_GetValue(&g_adc_handle);
}
該函數(shù)先是調(diào)用我們自己定義的adc_channel_set函數(shù)選擇ADC通道、設(shè)置轉(zhuǎn)換序列號和采樣時間等,接著調(diào)用HAL_ADC_Start啟動轉(zhuǎn)換,然后調(diào)用HAL_ADC_PollForConversion函數(shù)等待轉(zhuǎn)換完成,最后調(diào)用HAL_ADC_GetValue函數(shù)獲取轉(zhuǎn)換結(jié)果。
接下來要介紹的函數(shù)是獲取ADC某通道多次轉(zhuǎn)換結(jié)果平均值函數(shù),函數(shù)定義如下:
/**
* @brief 獲取通道ch的轉(zhuǎn)換值,取times次,然后平均
* @param ch : 通道號, 0~17
* @param times : 獲取次數(shù)
* @retval 通道ch的times次轉(zhuǎn)換結(jié)果平均值
*/
uint32_t adc_get_result_average(uint32_t ch, uint8_t times)
{
uint32_t temp_val = 0;
uint8_t t;
for (t = 0; t < times; t++) /* 獲取times次數(shù)據(jù) */
{
temp_val += adc_get_result(ch);
delay_ms(5);
}
return temp_val / times; /* 返回平均值 */
}
該函數(shù)用于獲取ADC多次轉(zhuǎn)換結(jié)果的平均值,從而提高準(zhǔn)確度。
最后在main函數(shù)里面編寫如下代碼:
int main(void)
{
uint16_t adcx;
float temp;
sys_stm32_clock_init(RCC_PLL_MUL9); /* 設(shè)置時鐘, 72Mhz */
delay_init(72); /* 延時初始化 */
usart_init(115200); /* 串口初始化為115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
adc_init(); /* 初始化ADC */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "ADC TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH1_VAL:", BLUE);
/* 先在固定位置顯示小數(shù)點 */
lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH1_VOL:0.000V", BLUE);
while (1)
{
/* 獲取通道5的轉(zhuǎn)換值,10次取平均 */
adcx = adc_get_result_average(ADC_ADCX_CHY, 10);
lcd_show_xnum(134, 110, adcx, 5, 16, 0, BLUE); /* 顯示ADCC采樣后的原始值 */
temp = (float)adcx * (3.3/4096);/* 獲取計算后的帶小數(shù)的實際電壓值,比如3.1111 */
adcx = temp; /* 賦值整數(shù)部分給adcx變量,因為adcx為u16整形 */
/* 顯示電壓值的整數(shù)部分,3.1111的話,這里就是顯示3 */
lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE);
temp -= adcx;/* 把已經(jīng)顯示的整數(shù)部分去掉,留下小數(shù)部分,比如3.1111-3=0.1111 */
temp *= 1000;/* 小數(shù)部分乘以1000,例如:0.1111就轉(zhuǎn)換為111.1,相當(dāng)于保留三位小數(shù)*/
/* 顯示小數(shù)部分(前面轉(zhuǎn)換為了整形顯示),這里顯示的就是111. */
lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE);
LED0_TOGGLE();
delay_ms(100);
}
}
此部分代碼,我們在TFTLCD模塊上顯示一些提示信息后,將每隔100ms讀取一次ADC通道1的轉(zhuǎn)換值,并顯示讀到的ADC值(數(shù)字量),以及其轉(zhuǎn)換成模擬量后的電壓值。同時控制LED0閃爍,以提示程序正在運行。ADC值的顯示簡單介紹一下:首先在液晶固定位置顯示了小數(shù)點,先計算出整數(shù)部分在小數(shù)點前面顯示,然后計算出小數(shù)部分,在小數(shù)點后面顯示。這樣就能在液晶上面顯示轉(zhuǎn)換結(jié)果的整數(shù)和小數(shù)部分。
30.2.4 下載驗證
下載代碼后,可以看到LCD顯示如圖30.2.4.1所示:
圖30.2.4.1單通道ADC采集實驗測試圖
上圖中,我們使用杜邦線將ADC和RV1排針連接,使得PA1連接到電位器上,測試的是電位器的電壓,并可以通過螺絲刀調(diào)節(jié)電位器改變電壓值,范圍:0~3.3V。LED0閃爍,提示程序運行。
大家也可以用杜邦線將ADC排針接到其它待測量的電壓點,看看測量到的電壓值是否準(zhǔn)確?但是要注意:一定要保證測試點的電壓在0~3.3V的電壓范圍,否則可能燒壞我們的ADC,甚至是整個主控芯片。
30.3 單通道ADC采集(DMA讀?。嶒?br> 本實驗我們來學(xué)習(xí)單通道ADC采集(DMA讀?。嶒?。本實驗使用規(guī)則組單通道的連續(xù)轉(zhuǎn)換模式,并且通過軟件觸發(fā),即由ADC_CR2寄存器的SWSTART位啟動。由于使用連續(xù)轉(zhuǎn)換模式,所以使用DMA讀取轉(zhuǎn)換結(jié)果的方式。下面先帶大家來了解本實驗要配置的寄存器。
30.3.1 ADC & DMA寄存器
本實驗我們很多的設(shè)置和單通道ADC采集實驗是一樣的,所以下面介紹寄存器的時候我們不會繼續(xù)全部都介紹,而是針對性選擇與單通道ADC采集實驗不同設(shè)置的ADC_CR2寄存器進(jìn)行介紹,其他的配置基本一樣的。另外因為我們用到DMA讀取數(shù)據(jù),所以還會介紹如何配置相關(guān)DMA的寄存器。
? ADC配置寄存器(ADC_CR2)
ADCx配置寄存器描述如圖30.3.1.1所示:
圖30.3.1.1 ADC_CR2寄存器
ADC_CR2寄存器中我們主要跟前面設(shè)置不同的有兩個位,分別如下:
DMA位用于配置使用DMA模式,本實驗該位置1。在單通道ADC采集實驗中,默認(rèn)設(shè)置為0,即不使用DMA模式,規(guī)則組轉(zhuǎn)換的結(jié)果存儲在ADC_DR寄存器,然后通過手動讀取ADC_DR寄存器的方式得到轉(zhuǎn)換結(jié)果。本實驗我們使用ADC的連續(xù)轉(zhuǎn)換模式,并通過DMA讀取轉(zhuǎn)換結(jié)果,這樣DMA就會自動在ADC_DR寄存器中讀取轉(zhuǎn)換結(jié)果。
CONT位用于設(shè)置單次轉(zhuǎn)換模式還是連續(xù)轉(zhuǎn)換模式,本實驗我們使用連續(xù)轉(zhuǎn)換模式,所以CONT位置1即可。
這里介紹ADC_CR2寄存器的這兩個位,其它請參考上一個實驗的配置。下面介紹DMA一些比較重要的寄存器配置。
? DMA通道x外設(shè)地址寄存器(DMA_CPARx)(x = 1…7)
DMA通道x外設(shè)地址寄存器描述如圖30.3.1.2所示:
圖30.3.1.2 DMA_CPARx寄存器
該寄存器存放的是DMA通道x外設(shè)地址。本實驗,我們需要通過DMA讀取ADC轉(zhuǎn)換后存放在ADC規(guī)則數(shù)據(jù)寄存器 (ADC_DR) 的結(jié)果數(shù)據(jù)。所以我們需要給DMA_CPARx寄存器寫入ADC_DR寄存器的地址。這樣配置后,DMA就會從ADC_DR寄存器的地址讀取ADC的轉(zhuǎn)換后的數(shù)據(jù)到某個內(nèi)存空間。這個內(nèi)存空間地址需要我們通過DMA_CMARx寄存器來設(shè)置,比如定義一個變量,把這個變量的地址值寫入該寄存器。
注意:DMA_CPARx寄存器受到寫保護(hù),只有DMA_CCRx寄存器中的EN為“0”時才可以寫入,即先要禁止通道開啟才可以寫入。
? DMA通道x存儲器地址寄存器(DMA_CMARx)(x = 1…7)
DMA通道x存儲器地址寄存器描述如圖30.3.1.3所示:
圖30.3.1.3 DMA_CMARx寄存器
該寄存器存放的是DMA通道x存儲器地址。同樣的,該寄存器也是受寫保護(hù),只有當(dāng)DMA_CCRx的EN位為0時才可以寫入。
? DMA通道x傳輸數(shù)量寄存器(DMA_CNDTRx)(x = 1…7)
DMA通道x傳輸數(shù)量寄存器描述如圖30.3.1.4所示:
圖30.3.1.4 DMA_CNDTRx
該寄存器控制著DMA通道x的每次傳輸所要傳輸?shù)臄?shù)據(jù)量。其設(shè)置范圍為0~65535。并且該寄存器的值隨著傳輸?shù)倪M(jìn)行而減少,當(dāng)該寄存器的值為0的時候就代表此次數(shù)據(jù)傳輸已經(jīng)全部發(fā)送完成。所以可以通過這個寄存器的值來獲取當(dāng)前DMA傳輸?shù)倪M(jìn)度。
其它的DMA寄存器我們就不一一介紹了,請大家看著寄存器源碼對照手冊理解,都不難。
30.3.2 硬件設(shè)計
- 例程功能
使用ADC采集(DMA讀取)通道1(PA1)上面的電壓,在LCD模塊上面顯示ADC轉(zhuǎn)換值以及換算成電壓后的電壓值。使用短路帽將ADC和RV1排針連接,使得PA1連接到電位器上,然后將ADC采集到的數(shù)據(jù)和轉(zhuǎn)換后的電壓值在TFTLCD屏中顯示。用戶可以通過調(diào)節(jié)電位器的旋鈕改變電壓值。LED0閃爍,提示程序運行。 - 硬件資源
1)LED燈
LED0 – PB5
2)串口1(PA9/PA10連接在板載USB轉(zhuǎn)串口芯片CH340上面)
3)正點原子 2.8/3.5/4.3/7/10寸TFTLCD模塊(僅限MCU屏,16位8080并口驅(qū)動)
4)ADC :通道1 – PA1
5)DMA(DMA1通道1) - 原理圖
ADC屬于STM32F103內(nèi)部資源,實際上我們只需要軟件設(shè)置就可以正常工作,另外還需要將待測量的電壓源連接到ADC通道上,以便ADC測量。本實驗,我們通過ADC1的通道1(PA1)來采集外部電壓值,開發(fā)板有一個電位器,可調(diào)節(jié)的電壓范圍是:0~3.3V。我們可以通過杜邦線將PA1與電位器連接,如下圖所示:
圖30.3.2.1 PA1(對應(yīng)ADC排針)與電位器示意圖
使用杜邦線將ADC和RV1排針連接好后,并下載程序后,就可以用螺絲刀調(diào)節(jié)電位器變換多種電壓值進(jìn)行測量。
有的朋友可能還想測量其它地方的電壓值,我們只需要1根杜邦線,一端接到ADC排針上,另外一端就接你要測試的電壓點。一定要保證測試點的電壓在0~3.3V的電壓范圍,否則可能燒壞我們的ADC,甚至是整個主控芯片。
30.3.3 程序設(shè)計
30.3.3.1 ADC & DMA的HAL庫驅(qū)動
單通道ADC采集實驗已經(jīng)介紹了一部分ADC的HAL庫API函數(shù),這里要介紹的是HAL_DMA_Start_IT和HAL_ADC_Start_DMA函數(shù)。
- HAL_DMA_Start_IT函數(shù)
啟動DMA傳輸并開啟相關(guān)中斷函數(shù),其聲明如下:
HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma,
uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
?函數(shù)描述:
用于啟動DMA傳輸,并開啟相關(guān)中斷,DMA1和DMA2都是用的這個函數(shù)。
?函數(shù)形參:
形參1是DMA_HandleTypeDef結(jié)構(gòu)體類型指針變量。
形參2是DMA傳輸?shù)脑吹刂贰?br> 形參3是DMA傳輸?shù)哪康牡刂贰?br> 形參4是要傳輸?shù)臄?shù)據(jù)項數(shù)目。
?函數(shù)返回值:
HAL_StatusTypeDef枚舉類型的值。 - HAL_ADC_Start_DMA函數(shù)
啟動ADC(DMA傳輸)方式函數(shù),其聲明如下:
HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc,
uint32_t pData, uint32_t Length);
?函數(shù)描述:
用于啟動ADC(DMA傳輸)方式的函數(shù)。
?函數(shù)形參:
形參1是ADC_HandleTypeDef結(jié)構(gòu)體類型指針變量。
形參2是ADC 采樣數(shù)據(jù)傳輸?shù)哪康牡刂贰?br> 形參3是要傳輸?shù)臄?shù)據(jù)項數(shù)目。
?函數(shù)返回值:
HAL_StatusTypeDef枚舉類型的值。
單通道ADC采集(DMA讀取)配置步驟
1)開啟ADCx和ADC通道對應(yīng)的IO時鐘,并配置該IO為模擬功能
首先開啟ADCx的時鐘,然后配置GPIO為模擬模式。本實驗我們默認(rèn)用到ADC1通道1,對應(yīng)IO是PA1,它們的時鐘開啟方法如下:
__HAL_RCC_ADC1_CLK_ENABLE (); / 使能ADC1時鐘 /
__HAL_RCC_GPIOA_CLK_ENABLE(); / 開啟GPIOA時鐘 */
2)初始化ADCx, 配置其工作參數(shù)
通過HAL_ADC_Init函數(shù)來設(shè)置ADCx時鐘分頻系數(shù)、分辨率、模式、掃描方式、對齊方式等信息。
注意:該函數(shù)會調(diào)用:HAL_ADC_MspInit回調(diào)函數(shù)來存放ADC及GPIO時鐘使能、GPIO初始化等代碼。我們也可以不存放在這個函數(shù)里,本實驗就沒用到這個MSP回調(diào)函數(shù)。
3)配置ADC通道并啟動AD轉(zhuǎn)換器
在HAL庫中,通過HAL_ADC_ConfigChannel函數(shù)來選擇要配置ADC的通道,并設(shè)置規(guī)則序列、采樣時間等。
4)初始化DMA
通過HAL_DMA_Init函數(shù)初始化DMA,包括配置通道,外設(shè)地址,存儲器地址,傳輸數(shù)據(jù)量等。
HAL庫為了處理各類外設(shè)的DMA請求,在調(diào)用相關(guān)函數(shù)之前,需要調(diào)用一個宏定義標(biāo)識符,來連接DMA和外設(shè)句柄。這個宏定義為__HAL_LINKDMA。
5)使能DMA對應(yīng)數(shù)據(jù)流中斷,配置DMA中斷優(yōu)先級并開啟中斷,啟動ADC和DMA
通過HAL_ADC_Start_DMA函數(shù)開啟ADC轉(zhuǎn)換,通過DMA傳輸結(jié)果。
通過HAL_DMA_Start_IT函數(shù)啟動DMA讀取,使能DMA中斷。
通過HAL_NVIC_EnableIRQ函數(shù)使能DMA數(shù)據(jù)流中斷。
通過HAL_NVIC_SetPriority函數(shù)設(shè)置中斷優(yōu)先級。
6)編寫中斷服務(wù)函數(shù)
DMA的每個數(shù)據(jù)流幾乎都有一個中斷服務(wù)函數(shù),比如DMA1_Channel1的中斷服務(wù)函數(shù)為DMA1_Channel1_IRQHandler。簡單的做法就是在,對應(yīng)的中斷服務(wù)函數(shù)里面,通過判斷相關(guān)的中斷標(biāo)志位的方式,完成中斷邏輯代碼,最后清楚該中斷標(biāo)志位,本實驗的做法就是如此。
還可以通過調(diào)用HAL庫提供的DMA中斷公用處理函數(shù)HAL_DMA_IRQHandler,然后定重新義相關(guān)的中斷回調(diào)處理函數(shù)。
30.3.3.2 程序流程圖
圖30.3.3.2.1 單通道ADC采集(DMA讀取)實驗程序流程圖
30.3.3.3 程序解析
由于本實驗用到DMA,所以在adc.h頭文件定義了以下一些宏定義:
/* ADC單通道/多通道 DMA采集 DMA及通道 定義
- 注意: ADC1的DMA通道只能是: DMA1_Channel1, 因此只要是ADC1, 這里是不能改動的
* ADC2不支持DMA采集
* ADC3的DMA通道只能是: DMA2_Channel5, 因此如果使用 ADC3 則需要修改
*/
#define ADC_ADCX_DMACx DMA1_Channel1
#define ADC_ADCX_DMACx_IRQn DMA1_Channel1_IRQn
#define ADC_ADCX_DMACx_IRQHandler DMA1_Channel1_IRQHandler
/*判斷DMA1_Channel1傳輸完成標(biāo)志, 是一個假函數(shù)形式, 不能當(dāng)函數(shù)使用, 只能用在if等語句里面*/
#define ADC_ADCX_DMACx_IS_TC() ( DMA1->ISR & (1 << 1) )
/* 清除 DMA1_Channel1 傳輸完成標(biāo)志 */
#define ADC_ADCX_DMACx_CLR_TC() do{ DMA1->IFCR |= 1 << 1; }while(0)
下面給大家介紹adc.c文件里面的函數(shù),首先是ADC DMA讀取初始化函數(shù)。
/**
* @brief ADC DMA讀取 初始化函數(shù)
* @param mar : 存儲器地址
* @retval 無
*/
void adc_dma_init(uint32_t mar)
{
GPIO_InitTypeDef gpio_init_struct;
RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
ADC_ChannelConfTypeDef adc_ch_conf = {0};
ADC_ADCX_CHY_CLK_ENABLE(); /* 使能ADCx時鐘 */
ADC_ADCX_CHY_GPIO_CLK_ENABLE(); /* 開啟GPIO時鐘 */
/* 大于DMA1_Channel7, 則為DMA2的通道了 */
if ((uint32_t)ADC_ADCX_DMACx > (uint32_t)DMA1_Channel7)
{
__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2時鐘使能 */
}
else
{
__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1時鐘使能 */
}
/* 設(shè)置ADC時鐘 */
adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; /* ADC外設(shè)時鐘 */
/* 分頻因子6時鐘為72M/6=12MHz */
adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;
HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); /* 設(shè)置ADC時鐘 */
/* 設(shè)置AD采集通道對應(yīng)IO引腳工作模式 */
gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN; /* ADC通道對應(yīng)的IO引腳 */
gpio_init_struct.Mode = GPIO_MODE_ANALOG; /* 模擬 */
HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);
/* 初始化DMA */
g_dma_adc_handle.Instance = ADC_ADCX_DMACx; /* 設(shè)置DMA通道 */
g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;/* 從外設(shè)到存儲器模式 */
g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外設(shè)非增量模式 */
g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存儲器增量模式 */
/* 外設(shè)數(shù)據(jù)長度:16位 */
g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
/* 存儲器數(shù)據(jù)長度:16位 */
g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
g_dma_adc_handle.Init.Mode = DMA_NORMAL; /* 外設(shè)流控模式 */
g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等優(yōu)先級 */
HAL_DMA_Init(&g_dma_adc_handle);
/* 將DMA與adc聯(lián)系起來 */
__HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle);
g_adc_dma_handle.Instance = ADC_ADCX; /* 選擇哪個ADC */
g_adc_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;/* 數(shù)據(jù)對齊方式:右對齊 */
g_adc_dma_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;/* 非掃描模式 */
g_adc_dma_handle.Init.ContinuousConvMode = ENABLE; /* 使能連續(xù)轉(zhuǎn)換模式 */
g_adc_dma_handle.Init.NbrOfConversion = 1;/* 范圍是1~16,這里用到1個規(guī)則序列 */
g_adc_dma_handle.Init.DiscontinuousConvMode = DISABLE;/* 禁止規(guī)則組間斷模式 */
/* 配置間斷模式的規(guī)則通道個數(shù),禁止規(guī)則通道組間斷模式后,此參數(shù)忽略 */
g_adc_dma_handle.Init.NbrOfDiscConversion = 0;
g_adc_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 軟件觸發(fā) */
HAL_ADC_Init(&g_adc_dma_handle); /* 初始化 */
HAL_ADCEx_Calibration_Start(&g_adc_dma_handle); /* 校準(zhǔn)ADC */
/* 配置ADC通道 */
adc_ch_conf.Channel = ADC_ADCX_CHY; /* 通道 */
adc_ch_conf.Rank = ADC_REGULAR_RANK_1; /* 序列 */
/* 采樣時間,設(shè)置最大采樣周期:239.5個ADC周期 */
adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf); /* 通道配置 */
/* 配置DMA數(shù)據(jù)流請求中斷優(yōu)先級 */
HAL_NVIC_SetPriority(ADC_ADCX_DMACx_IRQn, 3, 3);
HAL_NVIC_EnableIRQ(ADC_ADCX_DMACx_IRQn);
/* 啟動DMA,并開啟中斷 */
HAL_DMA_Start_IT(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0);
HAL_ADC_Start_DMA(&g_adc_dma_handle, &mar, 0); /* 開啟ADC,通過DMA傳輸結(jié)果 */
}
adc_dma_init函數(shù)包含了輸出通道對應(yīng)IO的初始代碼、NVIC、使能時鐘、ADC時鐘預(yù)分頻系數(shù)、ADC工作參數(shù)和ADC通道配置等代碼。下面來看看該函數(shù)的代碼內(nèi)容。
第一部分使能ADC、DMA和GPIO的時鐘。
第二部分配置ADC時鐘預(yù)分頻系數(shù)為6,得到ADC的輸入時鐘頻率是12MHz。
第三部分是設(shè)置ADC采集通道對應(yīng)IO引腳工作模式。
第四部分初始化DMA,并通過__HAL_LINKDMA宏定義將DMA相關(guān)的配置關(guān)聯(lián)到ADC的句柄中。
第五部分是初始化ADC,并校準(zhǔn)ADC。
第六部分是配置ADC通道。
第七部分是配置DMA數(shù)據(jù)流請求中斷優(yōu)先級,并使能該中斷。
第八部分是啟動DMA并開啟DMA中斷,以及啟動ADC并通過DMA傳輸轉(zhuǎn)換結(jié)果。
為了方便代碼的管理和移植性等,這里就沒有使用HAL_ADC_MspInit這個函數(shù)來存放使能時鐘、GPIO、NVIC相關(guān)的代碼,而是全部存放在adc_dma_init函數(shù)中。
接下來給大家介紹使能一次ADC DMA傳輸函數(shù),其定義如下:
/**
* @brief 使能一次ADC DMA傳輸
* @note 該函數(shù)用寄存器來操作,防止用HAL庫操作對其他參數(shù)有修改,也為了兼容性
* @param ndtr: DMA傳輸?shù)拇螖?shù)
* @retval 無
*/
void adc_dma_enable(uint16_t cndtr)
{
ADC_ADCX->CR2 &= ~(1 << 0); /* 先關(guān)閉ADC */
ADC_ADCX_DMACx->CCR &= ~(1 << 0); /* 關(guān)閉DMA傳輸 */
while (ADC_ADCX_DMACx->CCR & (1 << 0)); /* 確保DMA可以被設(shè)置 */
ADC_ADCX_DMACx->CNDTR = cndtr; /* DMA傳輸數(shù)據(jù)量 */
ADC_ADCX_DMACx->CCR |= 1 << 0; /* 開啟DMA傳輸 */
ADC_ADCX->CR2 |= 1 << 0; /* 重新啟動ADC */
ADC_ADCX->CR2 |= 1 << 22; /* 啟動規(guī)則轉(zhuǎn)換通道 */
}
該函數(shù)我們使用寄存器來操作,防止用HAL庫相關(guān)宏操作會對其它參數(shù)進(jìn)行修改,同時也是為了兼容后面的實驗。HAL_DMA_Start_IT函數(shù)已經(jīng)配置好了DMA傳輸?shù)脑吹刂泛湍繕?biāo)地址,本函數(shù)只需要調(diào)用ADC_ADCX_DMACx->CNDTR = cndtr;語句給DMA_CNDTRx寄存器寫入要傳輸?shù)臄?shù)據(jù)量,然后啟動DMA就可以傳輸了。
下面介紹的是ADC DMA采集中斷服務(wù)函數(shù),函數(shù)定義如下:
/**
* @brief ADC DMA采集中斷服務(wù)函數(shù)
* @param 無
* @retval 無
*/
void ADC_ADCX_DMACx_IRQHandler(void)
{
if (ADC_ADCX_DMACx_IS_TC())
{
g_adc_dma_sta = 1; /* 標(biāo)記DMA傳輸完成 */
ADC_ADCX_DMACx_CLR_TC(); /* 清除DMA1 通道1 傳輸完成中斷 */
}
}
在該函數(shù)里,通過判斷DMA傳輸完成標(biāo)志位是否是1,是1就給g_adc_dma_sta 變量賦值為1,標(biāo)記DMA傳輸完成,最后清除DMA的傳輸完成標(biāo)志位。
最后在main.c里面編寫如下代碼:
#define ADC_DMA_BUF_SIZE 100 /* ADC DMA采集 BUF大小 */
uint16_t g_adc_dma_buf[ADC_DMA_BUF_SIZE]; /* ADC DMA BUF */
extern uint8_t g_adc_dma_sta; /* DMA傳輸狀態(tài)標(biāo)志,0,未完成;1,已完成 */
int main(void)
{
uint16_t i;
uint16_t adcx;
uint32_t sum;
float temp;
HAL_Init(); /*初始化HAL庫*/
sys_stm32_clock_init(RCC_PLL_MUL9); /* 設(shè)置時鐘, 72Mhz */
delay_init(72); /* 延時初始化 */
usart_init(115200); /* 串口初始化為115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
adc_dma_init((uint32_t)&g_adc_dma_buf); /* 初始化ADC DMA采集 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "ADC DMA TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH1_VAL:", BLUE);
/* 先在固定位置顯示小數(shù)點 */
lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH1_VOL:0.000V", BLUE);
adc_dma_enable(ADC_DMA_BUF_SIZE); /* 啟動ADC DMA采集 */
while (1)
{
if (g_adc_dma_sta == 1)
{
/* 計算DMA 采集到的ADC數(shù)據(jù)的平均值 */
sum = 0;
for (i = 0; i < ADC_DMA_BUF_SIZE; i++) /* 累加 */
{
sum += g_adc_dma_buf[i];
}
adcx = sum / ADC_DMA_BUF_SIZE; /* 取平均值 */
/* 顯示結(jié)果 */
lcd_show_xnum(134, 110, adcx, 4, 16, 0,BLUE); /*顯示ADCC采樣后的原始值*/
temp=(float)adcx*(3.3/4096); /*獲取計算后的帶小數(shù)的實際電壓值,比如3.1111*/
adcx = temp; /* 賦值整數(shù)部分給adcx變量,因為adcx為u16整形 */
/* 顯示電壓值的整數(shù)部分,3.1111的話,這里就是顯示3 */
lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE);
temp -= adcx;/*把已經(jīng)顯示的整數(shù)部分去掉,留下小數(shù)部分,比如3.1111-3=0.1111*/
temp*=1000;/*小數(shù)部分乘以1000,例如:0.1111就轉(zhuǎn)換為111.1,相當(dāng)于保留三位小數(shù)*/
/* 顯示小數(shù)部分(前面轉(zhuǎn)換為了整形顯示),這里顯示的就是111. */
lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE);
g_adc_dma_sta = 0; /* 清除DMA采集完成狀態(tài)標(biāo)志 */
adc_dma_enable(ADC_DMA_BUF_SIZE); /* 啟動下一次ADC DMA采集 */
}
LED0_TOGGLE();
delay_ms(100);
}
}
此部分代碼,和單通道ADC采集實驗十分相似,只是這里使能了DMA傳輸數(shù)據(jù),DMA傳輸?shù)臄?shù)據(jù)存放在g_adc_dma_buf數(shù)組里,這里我們對數(shù)組的數(shù)據(jù)取平均值,減少誤差。在LCD屏顯示結(jié)果的處理和單通道ADC采集實驗一樣。首先在液晶固定位置顯示了小數(shù)點,先計算出整數(shù)部分在小數(shù)點前面顯示,然后計算出小數(shù)部分,在小數(shù)點后面顯示。這樣就能在液晶上面顯示轉(zhuǎn)換結(jié)果的整數(shù)和小數(shù)部分。
30.3.4 下載驗證
下載代碼后,可以看到LCD顯示如圖30.3.4.1所示:
圖30.3.4.1 單通道ADC采集(DMA讀?。嶒灉y試圖
上圖中,我們使用短路帽將ADC和RV1排針連接,使得PA1連接到電位器上,測試電位器的電壓,并可以通過螺絲刀調(diào)節(jié)電位器改變電壓值,范圍:0~3.3V。LED0閃爍,提示程序運行。
大家也可以用杜邦線將ADC排針接到其它待測量的電壓點,看看測量到的電壓值是否準(zhǔn)確?但是要注意:一定要保證測試點的電壓在0~3.3V的電壓范圍,否則可能燒壞我們的ADC,甚至是整個主控芯片。
30.4 多通道ADC采集(DMA讀?。嶒?br> 本實驗我們來學(xué)習(xí)多通道ADC采集(DMA讀?。嶒灐1緦嶒炇褂靡?guī)則組多通道的連續(xù)轉(zhuǎn)換模式,并且通過軟件觸發(fā),即由ADC_CR2寄存器的SWSTART位啟動。由于使用連續(xù)轉(zhuǎn)換模式,所以使用DMA讀取轉(zhuǎn)換結(jié)果的方式。下面先帶大家來了解本實驗要配置的寄存器。
30.4.1 ADC寄存器
本實驗我們很多的設(shè)置和單通道ADC采集(DMA讀?。嶒炇且粯拥?,所以下面介紹寄存器的時候我們不會繼續(xù)全部都介紹,而是針對性選擇與單通道ADC采集(DMA讀?。嶒灢煌O(shè)置的ADC_SQRx寄存器進(jìn)行介紹,其他的配置基本一樣的。另外我們用到DMA讀取數(shù)據(jù),配置上和單通道ADC采集(DMA讀取)實驗是一樣的。
ADC規(guī)則序列寄存器有四個(ADC_SQR1~ ADC_SQR3),具體怎么配置,需要看我們用多少個通道,比如本實驗我們使用6個通道同時采集ADC數(shù)據(jù),具體配置如下:
? ADC規(guī)則序列寄存器1(ADC_SQR1)
ADC規(guī)則序列寄存器1描述如圖30.4.1.1所示:
圖30.4.1.1 ADC_SQR1寄存器
L[3:0]位用于設(shè)置規(guī)則序列的長度,取值范圍:015,表示規(guī)則序列長度為116。本實驗使用到6個通道,所以設(shè)置這幾個位的值為5即可。
SQ13[4:0]SQ16[4:0]位設(shè)置規(guī)則組序列的第1316個轉(zhuǎn)換編號,第1~12個轉(zhuǎn)換編號的設(shè)置請查看ADC_SQR2和ADC_SQR3寄存器。設(shè)置過程非常簡單,忘記了請參考前面給大家整理出來的規(guī)則序列寄存器控制關(guān)系匯總表。
下面我們來看看本實驗是怎么設(shè)置的:SQ1[4:0]位賦值為0、SQ2[4:0]位賦值為1、SQ3[4:0]位賦值為2、SQ4[4:0]位賦值為3、SQ5[4:0]位賦值為4、SQ6[4:0]位賦值為5,即規(guī)則序列1到6分別對應(yīng)的輸入通道是0到5。SQ1~SQ6位都是在ADC_SQR3寄存器中配置。
30.4.2 硬件設(shè)計
- 例程功能
使用ADC1采集(DMA讀?。┩ǖ?\2\3\4\5\6的電壓,在LCD模塊上面顯示對應(yīng)的ADC轉(zhuǎn)換值以及換算成電壓后的電壓值。可以使用杜邦線連接PA0\PA1\PA2\PA3\PA4\PA5到你想測量的電壓源(0~3.3V),然后通過TFTLCD顯示的電壓值。LED0閃爍,提示程序運行。 - 硬件資源
1)LED燈
LED0 – PE5
2)串口1(PA9/PA10連接在板載USB轉(zhuǎn)串口芯片CH340上面)
3)正點原子 2.8/3.5/4.3/7/10寸TFTLCD模塊(僅限MCU屏,16位8080并口驅(qū)動)
4)ADC1 : 通道1–PA0、通道2–PA1、通道3–PA2、
通道4–PA3、通道5–PA4、通道6–PA5
5)DMA(DMA1通道1) - 原理圖
ADC和DMA屬于STM32F103內(nèi)部資源,實際上我們只需要軟件設(shè)置就可以正常工作,另外還需要將待測量的電壓源連接到ADC通道上,以便ADC測量。本實驗,我們通過ADC1的通道1\2\3\4\5\6來采集外部電壓值,并通過DMA來讀取。
30.4.3 程序設(shè)計
30.4.3.1 ADC的HAL庫驅(qū)動
本實驗用到的ADC的HAL庫API函數(shù)前面都介紹過,具體調(diào)用情況請看程序解析部分。下面介紹多通道ADC采集(DMA讀取)配置步驟。
多通道ADC采集(DMA讀?。┡渲貌襟E
1)開啟ADCx和ADC通道對應(yīng)的IO時鐘,并配置該IO為模擬功能
首先開啟ADCx的時鐘,然后配置GPIO為模擬模式。本實驗我們默認(rèn)用到ADC1通道0、1、2、3、4、5,對應(yīng)IO是PA0、PA1、PA2、PA3、PA4和PA5,它們的時鐘開啟方法如下:
__HAL_RCC_ADC1_CLK_ENABLE (); /* 使能ADC1時鐘 /
__HAL_RCC_GPIOA_CLK_ENABLE(); / 開啟GPIOA時鐘 */
2)初始化ADCx, 配置其工作參數(shù)
通過HAL_ADC_Init函數(shù)來設(shè)置ADCx時鐘分頻系數(shù)、分辨率、模式、掃描方式、對齊方式等信息。
注意:該函數(shù)會調(diào)用:HAL_ADC_MspInit回調(diào)函數(shù)來存放ADC及GPIO時鐘使能、GPIO初始化等代碼。我們也可以不存放在這個函數(shù)里,本實驗就沒用到這個MSP回調(diào)函數(shù)。
3)配置ADC通道并啟動AD轉(zhuǎn)換器
在HAL庫中,通過HAL_ADC_ConfigChannel函數(shù)來選擇要配置ADC的通道,并設(shè)置規(guī)則序列、采樣時間等。
配置好ADC通道之后,通過HAL_ADC_Start函數(shù)啟動AD轉(zhuǎn)換器。
4)初始化DMA
通過HAL_DMA_Init函數(shù)初始化DMA,包括配置通道,外設(shè)地址,存儲器地址,傳輸數(shù)據(jù)量等。
HAL庫為了處理各類外設(shè)的DMA請求,在調(diào)用相關(guān)函數(shù)之前,需要調(diào)用一個宏定義標(biāo)識符,來連接DMA和外設(shè)句柄。這個宏定義為__HAL_LINKDMA。
5)使能DMA對應(yīng)數(shù)據(jù)流中斷,配置DMA中斷優(yōu)先級,使能ADC,使能并啟動DMA
通過HAL_ADC_Start_DMA函數(shù)開啟ADC轉(zhuǎn)換,通過DMA傳輸結(jié)果。
通過HAL_DMA_Start_IT函數(shù)啟動DMA讀取,使能DMA中斷。
通過HAL_NVIC_EnableIRQ函數(shù)使能DMA數(shù)據(jù)流中斷。
通過HAL_NVIC_SetPriority函數(shù)設(shè)置中斷優(yōu)先級。
6)編寫中斷服務(wù)函數(shù)
DMA的每個數(shù)據(jù)流幾乎都有一個中斷服務(wù)函數(shù),比如DMA1_Channel1的中斷服務(wù)函數(shù)為DMA1_Channel1_IRQHandler。簡單的做法就是在,對應(yīng)的中斷服務(wù)函數(shù)里面,通過判斷相關(guān)的中斷標(biāo)志位的方式,完成中斷邏輯代碼,最后清楚該中斷標(biāo)志位,本實驗的做法就是如此。
還可以通過調(diào)用HAL庫提供的DMA中斷公用處理函數(shù)HAL_DMA_IRQHandler,然后定重新義相關(guān)的中斷回調(diào)處理函數(shù)。
30.4.3.2 程序流程圖
圖30.4.3.2.1 多通道ADC采集(DMA讀?。嶒灣绦蛄鞒虉D
30.4.3.3 程序解析
在本實驗中adc.h頭文件只是添加了一些函數(shù)聲明,下面開始介紹adc.c的函數(shù),本實驗只增加了一個函數(shù),ADC的N通道(6通道) DMA讀取初始化函數(shù),其定義如下:
/**
* @brief ADC N通道(6通道) DMA讀取 初始化函數(shù)
* @note 由于本函數(shù)用到了6個通道, 宏定義會比較多內(nèi)容,
* 因此,本函數(shù)就不采用宏定義方式來修改通道了,
* 直接在本函數(shù)里面修改, 這里我們默認(rèn)使用PA0~PA5這6個通道.
*
* 注意: 本函數(shù)還是使用 ADC_ADCX(默認(rèn)=ADC1)ADC_ADCX_DMACx(DMA1_Channel1) 及其相關(guān)定義。不要亂修改adc.h里面的這兩部分內(nèi)容, 必須在理解原理的基礎(chǔ)上進(jìn)行修改, 否則可能導(dǎo)致無法正常使用.
* @param mar : 存儲器地址
* @retval 無
*/
void adc_nch_dma_init(uint32_t mar)
{
GPIO_InitTypeDef gpio_init_struct;
RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
ADC_ChannelConfTypeDef adc_ch_conf = {0};
ADC_ADCX_CHY_CLK_ENABLE(); /* 使能ADCx時鐘 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 開啟GPIOA時鐘 */
/* 大于DMA1_Channel7, 則為DMA2的通道了 */
if ((uint32_t)ADC_ADCX_DMACx > (uint32_t)DMA1_Channel7)
{
__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2時鐘使能 */
}
else
{
__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1時鐘使能 */
}
/* 設(shè)置ADC時鐘 */
adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; /* ADC外設(shè)時鐘 */
/* 分頻因子6時鐘為72M/6=12MHz */
adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;
HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); /* 設(shè)置ADC時鐘 */
/*
設(shè)置ADC1通道0~5對應(yīng)的IO口模擬輸入
AD采集引腳模式設(shè)置,模擬輸入
PA0對應(yīng) ADC1_IN0
PA1對應(yīng) ADC1_IN1
PA2對應(yīng) ADC1_IN2
PA3對應(yīng) ADC1_IN3
PA4對應(yīng) ADC1_IN4
PA5對應(yīng) ADC1_IN5
*/
gpio_init_struct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3
|GPIO_PIN_4|GPIO_PIN_5; /* GPIOA0~5 */
gpio_init_struct.Mode = GPIO_MODE_ANALOG; /* 模擬 */
HAL_GPIO_Init(GPIOA, &gpio_init_struct);
/* 初始化DMA */
g_dma_nch_adc_handle.Instance = ADC_ADCX_DMACx; /* 設(shè)置DMA通道 */
/* 從外設(shè)到存儲器模式 */
g_dma_nch_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
g_dma_nch_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外設(shè)非增量模式 */
g_dma_nch_adc_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存儲器增量模式 */
/* 外設(shè)數(shù)據(jù)長度:16位 */
g_dma_nch_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
/* 存儲器數(shù)據(jù)長度:16位 */
g_dma_nch_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
g_dma_nch_adc_handle.Init.Mode = DMA_NORMAL; /* 外設(shè)流控模式 */
g_dma_nch_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;/* 中等優(yōu)先級 */
HAL_DMA_Init(&g_dma_nch_adc_handle);
/* 將DMA與adc聯(lián)系起來 */
__HAL_LINKDMA(&g_adc_nch_dma_handle, DMA_Handle, g_dma_nch_adc_handle);
/* 初始化ADC */
g_adc_nch_dma_handle.Instance = ADC_ADCX; /* 選擇哪個ADC */
g_adc_nch_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 數(shù)據(jù)右對齊 */
g_adc_nch_dma_handle.Init.ScanConvMode = ADC_SCAN_ENABLE; /* 使能掃描模式 */
g_adc_nch_dma_handle.Init.ContinuousConvMode = ENABLE; /* 使能連續(xù)轉(zhuǎn)換 */
/* 賦值范圍是1~16,本實驗用到6個規(guī)則通道序列 */
g_adc_nch_dma_handle.Init.NbrOfConversion = 6;
/* 禁止規(guī)則組間斷模式 */
g_adc_nch_dma_handle.Init.DiscontinuousConvMode = DISABLE;
/* 配置間斷模式的規(guī)則通道個數(shù),禁止規(guī)則通道組間斷模式后,此參數(shù)忽略 */
g_adc_nch_dma_handle.Init.NbrOfDiscConversion = 0;
g_adc_nch_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;/* 軟件觸發(fā) */
HAL_ADC_Init(&g_adc_nch_dma_handle); /* 初始化 */
HAL_ADCEx_Calibration_Start(&g_adc_nch_dma_handle); /* 校準(zhǔn)ADC */
/* 配置ADC通道 */
adc_ch_conf.Channel = ADC_CHANNEL_0; /* 配置使用的ADC通道 */
adc_ch_conf.Rank = ADC_REGULAR_RANK_1; /* 采樣序列里的第1個 */
/* 采樣時間,設(shè)置最大采樣周期:239.5個ADC周期 */
adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道 */
adc_ch_conf.Channel = ADC_CHANNEL_1; /* 配置使用的ADC通道 */
adc_ch_conf.Rank = ADC_REGULAR_RANK_2; /* 采樣序列里的第2個 */
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道 */
adc_ch_conf.Channel = ADC_CHANNEL_2; /* 配置使用的ADC通道 */
adc_ch_conf.Rank = ADC_REGULAR_RANK_3; /* 采樣序列里的第3個 */
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道 */
adc_ch_conf.Channel = ADC_CHANNEL_3; /* 配置使用的ADC通道 */
adc_ch_conf.Rank = ADC_REGULAR_RANK_4; /* 采樣序列里的第4個 */
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道 */
adc_ch_conf.Channel = ADC_CHANNEL_4; /* 配置使用的ADC通道 */
adc_ch_conf.Rank = ADC_REGULAR_RANK_5; /* 采樣序列里的第5個 */
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); /* 配置ADC通道 */
adc_ch_conf.Channel = ADC_CHANNEL_5; /* 配置使用的ADC通道 */
adc_ch_conf.Rank = ADC_REGULAR_RANK_6; /* 采樣序列里的第6個 */
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道 */
/* 配置DMA數(shù)據(jù)流請求中斷優(yōu)先級 */
HAL_NVIC_SetPriority(ADC_ADCX_DMACx_IRQn, 3, 3);
HAL_NVIC_EnableIRQ(ADC_ADCX_DMACx_IRQn);
/* 啟動DMA,并開啟中斷 */
HAL_DMA_Start_IT(&g_dma_nch_adc_handle, (uint32_t)&ADC1->DR, mar, 0);
/* 開啟ADC,通過DMA傳輸結(jié)果 */
HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, &mar, 0);
}
adc_nch_dma_init函數(shù)包含了輸出通道對應(yīng)IO的初始代碼、NVIC、使能時鐘、ADC時鐘預(yù)分頻系數(shù)、ADC工作參數(shù)和ADC通道配置等代碼。大部分代碼和單通道ADC采集(DMA讀取)實驗一樣,下面來看看該函數(shù)的代碼內(nèi)容。
第一部分使能ADC、DMA和GPIO的時鐘。
第二部分配置ADC時鐘預(yù)分頻系數(shù)為6,得到ADC的輸入時鐘頻率是12MHz。
第三部分是設(shè)置ADC采集通道對應(yīng)IO引腳工作模式,這里用到6個通道。
第四部分初始化DMA,并通過__HAL_LINKDMA宏定義將DMA相關(guān)的配置關(guān)聯(lián)到ADC的句柄中。
第五部分是初始化ADC,并校準(zhǔn)ADC。
第六部分是配置ADC通道,這里有6個通道需要配置。
第七部分是配置DMA數(shù)據(jù)流請求中斷優(yōu)先級,并使能該中斷。
第八部分是啟動DMA并開啟DMA中斷,以及啟動ADC并通過DMA傳輸轉(zhuǎn)換結(jié)果。
為了方便代碼的管理和移植性等,這里就沒有使用HAL_ADC_MspInit這個函數(shù)來存放使能時鐘、GPIO、NVIC相關(guān)的代碼,而是全部存放在adc_nch_dma_init函數(shù)中。
最后在main.c里面編寫如下代碼:
#define ADC_DMA_BUF_SIZE 50 * 6 /* ADC DMA采集BUF大小, 應(yīng)等于ADC通道數(shù)的整數(shù)倍 */
uint16_t g_adc_dma_buf[ADC_DMA_BUF_SIZE]; /* ADC DMA BUF */
extern uint8_t g_adc_dma_sta; /* DMA傳輸狀態(tài)標(biāo)志, 0,未完成; 1, 已完成 */
int main(void)
{
uint16_t i,j;
uint16_t adcx;
uint32_t sum;
float temp;
sys_stm32_clock_init(9); /* 設(shè)置時鐘, 72Mhz */
delay_init(72); /* 延時初始化 */
usart_init(72, 115200); /* 串口初始化為115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
adc_nch_dma_init((uint32_t)&g_adc_dma_buf); /* 初始化ADC DMA采集 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "ADC 6CH DMA TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 12, 12, "ADC1_CH0_VAL:", BLUE);
/* 先在固定位置顯示小數(shù)點 */
lcd_show_string(30, 122, 200, 12, 12, "ADC1_CH0_VOL:0.000V", BLUE);
lcd_show_string(30, 140, 200, 12, 12, "ADC1_CH1_VAL:", BLUE);
/* 先在固定位置顯示小數(shù)點 */
lcd_show_string(30, 152, 200, 12, 12, "ADC1_CH1_VOL:0.000V", BLUE);
lcd_show_string(30, 170, 200, 12, 12, "ADC1_CH2_VAL:", BLUE);
/* 先在固定位置顯示小數(shù)點 */
lcd_show_string(30, 182, 200, 12, 12, "ADC1_CH2_VOL:0.000V", BLUE);
lcd_show_string(30, 200, 200, 12, 12, "ADC1_CH3_VAL:", BLUE);
/* 先在固定位置顯示小數(shù)點 */
lcd_show_string(30, 212, 200, 12, 12, "ADC1_CH3_VOL:0.000V", BLUE);
lcd_show_string(30, 230, 200, 12, 12, "ADC1_CH4_VAL:", BLUE);
/* 先在固定位置顯示小數(shù)點 */
lcd_show_string(30, 242, 200, 12, 12, "ADC1_CH4_VOL:0.000V", BLUE);
lcd_show_string(30, 260, 200, 12, 12, "ADC1_CH5_VAL:", BLUE);
/* 先在固定位置顯示小數(shù)點 */
lcd_show_string(30, 272, 200, 12, 12, "ADC1_CH5_VOL:0.000V", BLUE);
adc_dma_enable(ADC_DMA_BUF_SIZE); /* 啟動ADC DMA采集 */
while (1)
{
if (g_adc_dma_sta == 1)
{
/* 循環(huán)顯示通道0~通道5的結(jié)果 */
for(j = 0; j < 6; j++) /* 遍歷6個通道 */
{
sum = 0; /* 清零 */
for (i = 0; i < ADC_DMA_BUF_SIZE / 6; i++)
{/* 每個通道采集了10次數(shù)據(jù),進(jìn)行10次累加 */
sum += g_adc_dma_buf[(6 * i) + j]; /* 相同通道的轉(zhuǎn)換數(shù)據(jù)累加 */
}
adcx = sum / (ADC_DMA_BUF_SIZE / 6); /* 取平均值 */
/* 顯示結(jié)果 */
/* 顯示ADCC采樣后的原始值 */
lcd_show_xnum(108, 120 + (j * 30), adcx, 4, 12, 0, BLUE);
/* 獲取計算后的帶小數(shù)的實際電壓值,比如3.1111 */
temp = (float)adcx * (3.3 / 4096);
adcx = temp; /* 賦值整數(shù)部分給adcx變量,因為adcx為u16整形 */
/* 顯示電壓值的整數(shù)部分,3.1111的話,這里就是顯示3 */
lcd_show_xnum(108, 122 + (j * 30), adcx, 1, 12, 0, BLUE);
/* 把已經(jīng)顯示的整數(shù)部分去掉,留下小數(shù)部分,比如3.1111-3=0.1111 */
temp -= adcx;
/* 小數(shù)部分乘以1000,例如:0.1111就轉(zhuǎn)換為111.1,相當(dāng)于保留三位小數(shù)。 */
temp *= 1000;
/* 顯示小數(shù)部分(前面轉(zhuǎn)換為了整形顯示),這里顯示的就是111. */
lcd_show_xnum(120, 122 + (j * 30), temp, 3, 12, 0X80, BLUE);
}
g_adc_dma_sta = 0; /* 清除DMA采集完成狀態(tài)標(biāo)志 */
adc_dma_enable(ADC_DMA_BUF_SIZE); /* 啟動下一次ADC DMA采集 */
}
LED0_TOGGLE();
delay_ms(100);
}
}
這里使用了DMA傳輸數(shù)據(jù),DMA傳輸?shù)臄?shù)據(jù)存放在g_adc_dma_buf數(shù)組里,該數(shù)組的大小是50 * 6。本實驗用到6個通道,每個通道使用50個uint16_t大小的空間存放ADC的結(jié)果。輸入通道0的轉(zhuǎn)換數(shù)據(jù)存放在g_adc_dma_buf[0]到g_adc_dma_buf[49],輸入通道1的轉(zhuǎn)換數(shù)據(jù)存放在g_adc_dma_buf[50]到g_adc_dma_buf[99],后面的以此類推。然后對數(shù)組的每個通道的數(shù)據(jù)取平均值,減少誤差。最后在LCD屏上顯示ADC的轉(zhuǎn)換值和換算成電壓后的電壓值。
30.4.4 下載驗證
下載代碼后,LED0閃爍,提示程序運行??梢钥吹絃CD顯示如圖30.4.4.1所示:
圖30.4.4.1 多通道ADC采集(DMA讀?。嶒灉y試圖
使用ADC1采集(DMA讀取)通道0\1\2\3\4\5的電壓,在LCD模塊上面顯示對應(yīng)的ADC轉(zhuǎn)換值以及換算成電壓后的電壓值??梢允褂枚虐罹€連接PA0\PA1\PA2\PA3\PA4\PA5到你想測量的電壓源(0~3.3V)。
這6個通道對應(yīng)引出來的引腳PA0\PA1\PA2\PA3\PA4\PA5在開發(fā)板上的位置,如下圖所示:
圖30.4.4.2 ADC1的通道0\1\2\3\4\5引腳在開發(fā)板位置示意圖
這六個通道可以同時測量不同測試點的電壓,只需要用杜邦線分別接到不同的電壓測試點即可。注意:一定要保證測試點的電壓在0~3.3V的電壓范圍,否則可能燒壞我們的ADC,甚至是整個主控芯片。
30.5 單通道ADC過采樣(16位分辨率)實驗
本實驗我們來學(xué)習(xí)單通道ADC過采樣(16位分辨率)實驗。STM32F103自帶的ADC分辨率只有12位,雖然可以滿足一般的應(yīng)用,但是有些場合可能需要更高的分辨率,怎么辦呢?可以使用外部專用的ADC,或者換一個帶更高分辨率ADC的主控芯片。這樣做往往會增加額外的成本,那么有沒有其它辦法呢?答案是有的,可以通過引入過采樣技術(shù)來實現(xiàn)。
ADC過采樣技術(shù),是利用ADC多次采集的方式,來提高ADC分辨率。下面,簡單介紹一下怎么提高ADC測量的分辨率?
下面直接給大家介紹一個方程,根據(jù)要增加分辨率計算過采樣頻率方程,方程如下:
fos = 4w ? fs
其中,w是希望增加的分辨率位數(shù),fs是初始采樣頻率要求,fos是過采樣頻率。
方程的推導(dǎo)過程比較復(fù)雜,這里就不帶大家去推導(dǎo),感興趣的朋友可以通過下面這個鏈接自行學(xué)習(xí):https://max.book118.com/html/2018/0506/165038217.shtm。
由該方程可以知道,采樣速度每提高4倍,分辨率位數(shù)可以提高1位。結(jié)合ADC的實際情況,換個思路來說,分辨率位數(shù)每提高1位,如果采樣頻率不變的情況下,那么采樣速度就會降低4倍。本實驗要求得到16位分辨率,即需要增加4位分辨率,那么采樣速度就會降低256倍,即需要采集256次才能得出1次數(shù)據(jù),相當(dāng)于ADC速度慢了256倍。
理論上只要ADC足夠快,我們可以無限提高ADC精度,但實際上ADC并不是無限快的,而且由于ADC性能限制,并不是位數(shù)無限提高,結(jié)果就越好,需要根據(jù)自己的實際需求和ADC的實際性能來權(quán)衡的。
下面來看一下我們怎么實現(xiàn)單通道ADC過采樣(16位分辨率)實驗的?。
30.5.1 ADC寄存器
本實驗我們很多的設(shè)置和單通道ADC采集(DMA讀取)實驗是一樣的,代碼實現(xiàn)也是基于該實驗實現(xiàn)的,寄存器的介紹請參考前面的ADC實驗。
30.5.2 硬件設(shè)計
- 例程功能
使用ADC1通道1(PA1),通過軟件方式實現(xiàn)16位分辨率采集外部電壓,并在LCD模塊上面顯示對應(yīng)的ADC轉(zhuǎn)換值以及換算成電壓后的電壓值??梢允褂枚虐罹€連接PA1到你想測量的電壓源(0~3.3V),然后通過TFTLCD顯示的電壓值。LED0閃爍,提示程序運行。 - 硬件資源
1)LED燈
LED0 – PB5
2)串口1(PA9/PA10連接在板載USB轉(zhuǎn)串口芯片CH340上面)
3)正點原子 2.8/3.5/4.3/7/10寸TFTLCD模塊(僅限MCU屏,16位8080并口驅(qū)動)
4)ADC1 :通道1 – PA1 - 原理圖
ADC屬于STM32F103內(nèi)部資源,實際上我們只需要軟件設(shè)置就可以正常工作,另外還需要將待測量的電壓源連接到ADC通道上,以便ADC測量。本實驗,我們通過ADC1通道1(PA1)來采集外部電壓值。開發(fā)板有一個電位器,可調(diào)節(jié)的電壓范圍是:0~3.3V,可以通過斷路帽將PA1與電位器連接,從而測量電位器的電壓。
30.5.3 程序設(shè)計
30.5.3.1 ADC的HAL庫驅(qū)動
本實驗用到的ADC的HAL庫API函數(shù)前面都介紹過,具體調(diào)用情況請看程序解析部分。
30.5.3.2 程序流程圖
圖30.5.3.2.1 單通道ADC過采樣(16位分辨率)實驗程序流程圖
30.5.3.3 程序解析
這里我們只講解核心代碼,詳細(xì)的源碼請大家參考光盤本實驗對應(yīng)源碼。ADC驅(qū)動源碼包括兩個文件:adc.c和adc.h。本實驗沿用前面實驗中的函數(shù),并沒有改動。
下面介紹一下main.c里面編寫的代碼:
/* ADC過采樣次數(shù), 這里提高4bit分辨率, 需要256倍采樣 */
#define ADC_OVERSAMPLE_TIMES 256
/* ADC DMA采集 BUF大小, 應(yīng)等于過采樣次數(shù)的整數(shù)倍 */
#define ADC_DMA_BUF_SIZE ADC_OVERSAMPLE_TIMES * 10
uint16_t g_adc_dma_buf[ADC_DMA_BUF_SIZE]; /* ADC DMA BUF */
extern uint8_t g_adc_dma_sta; /* DMA傳輸狀態(tài)標(biāo)志,0,未完成; 1,已完成 */
extern ADC_HandleTypeDef g_adc_dma_handle;/* ADC(DMA讀?。┚浔?*/
int main(void)
{
uint16_t i;
uint32_t adcx;
uint32_t sum;
float temp;
HAL_Init(); /* 初始化HAL庫 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 設(shè)置時鐘, 72Mhz */
delay_init(72); /* 延時初始化 */
usart_init(115200); /* 串口初始化為115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
adc_dma_init((uint32_t)&g_adc_dma_buf); /* 初始化ADC DMA采集 */
/* 設(shè)置ADCX對應(yīng)通道采樣時間為1.5個時鐘周期, 已達(dá)到最高的采集速度 */
adc_channel_set(&g_adc_handle, ADC_ADCX_CHY, ADC_REGULAR_RANK_1,
ADC_SAMPLETIME_1CYCLE_5);
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "ADC OverSample TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH1_VAL:", BLUE);.
/* 先在固定位置顯示小數(shù)點 */
lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH1_VOL:0.000V", BLUE);
adc_dma_enable(ADC_DMA_BUF_SIZE); /* 啟動ADC DMA采集 */
while (1)
{
if (g_adc_dma_sta == 1)
{
/* 計算DMA 采集到的ADC數(shù)據(jù)的平均值 */
sum = 0;
for (i = 0; i < ADC_DMA_BUF_SIZE; i++) /* 累加 */
{
sum += g_adc_dma_buf[i];
}
adcx = sum / (ADC_DMA_BUF_SIZE / ADC_OVERSAMPLE_TIMES); /* 取平均值 */
/* 除以2^4倍, 得到12+4位 ADC精度值, 注意: 提高 N bit精度, 需要 >> N */
adcx >>= 4;
/* 顯示ADCC采樣后的原始值 */
lcd_show_xnum(134, 110, adcx, 5, 16, 0, BLUE);
/* 獲取計算后的帶小數(shù)的實際電壓值,比如3.1111 */
temp = (float)adcx * (3.3 / 65536);
adcx = temp; /* 賦值整數(shù)部分給adcx變量,因為adcx為u16整形 */
/* 顯示電壓值的整數(shù)部分,3.1111的話,這里就是顯示3 */
lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE);
temp -= adcx;/* 把已經(jīng)顯示的整數(shù)部分去掉,留下小數(shù)部分,比如3.1111-3=0.1111 */
temp *= 1000;/*小數(shù)部分乘以1000,例如:0.1111就轉(zhuǎn)換為111.1,相當(dāng)于保留三位小數(shù)*/
/* 顯示小數(shù)部分(前面轉(zhuǎn)換為了整形顯示),這里顯示的就是111. */
lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE);
g_adc_dma_sta = 0; /* 清除DMA采集完成狀態(tài)標(biāo)志 */
adc_dma_enable(ADC_DMA_BUF_SIZE); /* 啟動下一次ADC DMA采集 */
}
LED0_TOGGLE();
delay_ms(100);
}
}
上面的代碼中,ADC_OVERSAMPLE_TIMES宏定義表示為了提高4位分辨率,ADC需要進(jìn)行256次采樣才能得的一次16位分辨率的數(shù)據(jù)。為了減少誤差,ADC_DMA_BUF_SIZE宏定義是ADC_OVERSAMPLE_TIMES的10倍,為了后期取16位轉(zhuǎn)換結(jié)果平均值的。g_adc_dma_buf數(shù)組是uint16_t類型的,用于存放轉(zhuǎn)換結(jié)果。
為了提高ADC的采樣速度,調(diào)用adc_channel_set函數(shù)將采樣時間調(diào)整為1.5個ADC時鐘周期,以得到最高的采樣速度。
adcx = sum / (ADC_DMA_BUF_SIZE / ADC_OVERSAMPLE_TIMES);語句可以得到ADC采樣10次的16位分辨率轉(zhuǎn)換結(jié)果的平均值。adcx >>= 4;語句對該平均值右移4位,這個過程通常被稱為抽取。這樣就可以得到16位有用的數(shù)據(jù),該數(shù)據(jù)的取值范圍是0~65535,這個操作被稱為累加和轉(zhuǎn)儲。
接下來的代碼就是在LCD屏顯示轉(zhuǎn)換值和換算的電壓值,以及讓LED0閃爍,提示系統(tǒng)正在運行。
30.5.4 下載驗證
下載代碼后,LED0閃爍,提示程序運行。可以看到LCD顯示如圖30.5.4.1所示:文章來源:http://www.zghlxwxcb.cn/news/detail-775164.html
圖30.5.4.1 單通道ADC過采樣(16位分辨率)實驗測試圖
上圖中,我們使用短路帽將ADC和RV1排針連接,使得PA1連接到電位器上,測試電位器的電壓,并可以通過螺絲刀調(diào)節(jié)電位器改變電壓值,范圍:0~3.3V。LED0閃爍,提示程序運行。
大家也可以用杜邦線將ADC排針接到其它待測量的電壓點,看看測量到的電壓值是否準(zhǔn)確?但是要注意:一定要保證測試點的電壓在0~3.3V的電壓范圍,否則可能燒壞我們的ADC,甚至是整個主控芯片。文章來源地址http://www.zghlxwxcb.cn/news/detail-775164.html
到了這里,關(guān)于【正點原子STM32連載】 第三十章 ADC實驗 摘自【正點原子】STM32F103 戰(zhàn)艦開發(fā)指南V1.2的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!