硬體:STM32F103VCT6
開發工具:Keil uVision4
下載下傳調試工具:ARM仿真器
網上資料很多,這裡做一個詳細的整合。(也不是很詳細,但很通俗)。
所用的晶片内嵌3個12位的模拟/數字轉換器(ADC),每個ADC共用多達16個外部通道,2個内部通道。
3個:代表ADC1、ADC2、ADC3(下圖是晶片固件庫的截圖)
12位:也叫ADC分辨率、采樣精度。先來看看二進制的12位可表示0-4095個數,也就是說轉換器通過采集轉換所得到的最大值是4095,如:“111111111111”=4095,那麼我們怎麼通過轉換器轉換出來的值得到實際的電壓值呢?如果我們要轉換的電壓範圍是0v-3.3v的話,轉換器就會把0v-3.3v平均分成4096份。設轉換器所得到的值為x,所求電壓值為y。
那麼就有:
16個外部通道:簡單的說就是晶片上有16個引腳是可以接到模拟電壓上進行電壓值檢測的。16個通道不是獨立的配置設定給3個轉換器(ADC1、ADC2、ADC3)使用,有些通道是被多個轉換器共用的。首先看看16個通道在固件庫的宏定義(寫代碼要看的):
到這裡大家可能會有疑問,每個通道到底對應哪個引腳呢?下面先給出部分引腳圖:
16個通道的引腳都在上面的圖中,拿其中的一個進行說明:
ADC123_IN10:字母“ADC”不用多說,“123”代表它被3個(ADC1、ADC2、ADC3)轉換器共用的引腳,“10”對應剛才那張宏定義圖裡面的ADC_Channel_10,這樣就能找到每個通道對應的引腳了。
2個内部通道:一個是内部溫度傳感器,一個是内部參考電壓。
在某個項目中要用到晶片裡面的AD轉換器,那麼要怎麼寫應用代碼?(以下是代碼講解)
晶片固件的庫函數為我們提供了很多封裝好的函數,隻要運用它提供的函數接口就可以了,宏觀上來講就搞懂兩個事情就行了:
初始化(設定用的哪個引腳、單通道、還是多通道同時轉換、是否使用DMA等配置)?
怎麼讓轉換器進行一次資料擷取?
以下分别講述三種不同方式(單通道、多通道、基于DMA的多通道采集)的ADC應用執行個體:
void Adc_Config(void)
{
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1, ENABLE );
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //72M/6=12, ADC的采樣時鐘最快14MHz
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //GPIO_Mode_AIN:模拟輸入(還有其他什麼模式?請看下面的附錄圖1)
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_DeInit(ADC1); //複位,将ADC1相關的寄存器設為預設值
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //工作模式:ADC1和ADC2獨立工作模式 (還有其他什麼模式?請看下面的附錄圖2)
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //數模轉換工作:掃描(多通道)模式=ENABLE、單次(單通道)模式=DISABLE
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//數模轉換工作:連續=ENABLE、單次=DISABLE
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //ADC轉換由軟體觸發啟動 (還有其他什麼模式?請看下面的附錄圖3)
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC資料右對齊 除了右就是左:ADC_DataAlign_Left
ADC_InitStructure.ADC_NbrOfChannel = 1; //順序進行規則轉換的ADC通道的數目 範圍是1-16
ADC_Init(ADC1, &ADC_InitStructure); //根據ADC_InitStruct中指定的參數初始化外設ADC1的寄存器
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5 );
ADC_Cmd(ADC1, ENABLE); //使能指定的ADC 注意:函數ADC_Cmd隻能在其他ADC設定函數之後被調用
ADC_ResetCalibration(ADC1); //重置指定的ADC的校準寄存器
while(ADC_GetResetCalibrationStatus(ADC1)); //等待上一步操作完成
ADC_StartCalibration(ADC1); //開始指定ADC的校準狀态
while(ADC_GetCalibrationStatus(ADC1));//等待上一步操作按成
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
初始化完成之後,在主函數中:
void main(void)
{
float ADC_ConvertedValue;
float ADC_ConvertedValueLocal;
Adc_Config();
while(1)
{
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //啟動轉換
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC )); //等待轉換完成
ADC_ConvertedValue=ADC_GetConversionValue(ADC1); //擷取轉換結果*ADC_ConvertedValue*
ADC_ConvertedValueLocal=(float)ADC_ConvertedValue*(3.3/4096); //計算出實際電壓值*ADC_ConvertedValueLocal*
//這裡适當加上一些延遲
//最好連續轉換幾次 取平均值 這裡就省略寫了 點到為止
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
附錄圖1-GPIO_Mode值:
附錄圖2-ADC_Mode值:
附錄圖3-ADC_ExternalTrigConv值:
附錄圖4-ADC_SampleTime值:
對于一些剛接觸stm32的人來說,看了上面的代碼可能還會有很多疑問。
為什麼要使能時鐘?時鐘到底設定多少才合适?
對于ADC_GetConversionValue(ADC1)這個函數參數并沒有指定那個通道,如果多個通道同時使用CAN1轉換器轉換時怎麼擷取每個通道的值?
第一個問題,所有的外設都要使能時鐘,時鐘源分為外部時鐘和内部時鐘,外部時鐘比如接8MHz晶振,内部時鐘就在晶片内部內建,時鐘源為所有的時序電路提供基本的脈沖信号。時鐘源好比是一顆跳動的心髒,它按照一定的頻率在跳動,所有的器官(外設)要跟心髒(時鐘源)橋接起來才能工作,但不同的外設需要的頻率不同,是以在時鐘源跟外設之中常常還會有一些分頻器或者倍頻器,以實作對頻率的衰減或增強。還想了解更多專業的解釋可以去研究stm32的時鐘樹圖。
第二個問題,回答這個問題那麼就等于開始介紹多通道轉換怎麼實作了,看下圖
由圖了解,一個ADC轉換器隻能選擇轉換一個通道,那麼對比單通道我們隻需做一下改變(以雙通道為例):
1.在void Adc_Config(void)函數裡面添加:
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
1
2
3
配置多一個IO(PA1)口, 也就是通道1。
2.在void Adc_Config(void)函數裡面添加:
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5 );
1
2
先不指定ADC轉換通道。
3.在主函數循環裡改為:
while(1)
{
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5 );
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));
ADC_ConvertedValue=ADC_GetConversionValue(ADC1);
ADC_ConvertedValueLocal=(float)ADC_ConvertedValue*(3.3/4096);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5 );
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));
ADC_ConvertedValue=ADC_GetConversionValue(ADC1);
ADC_ConvertedValueLocal=(float)ADC_ConvertedValue*(3.3/4096);
//加入适當延時
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
完成以上三步就能把單通道擴充到雙通道(或者更多個通道)。不過還有一種基于DMA的多通道轉換更加合适。
首先簡單介紹DMA,DMA(Direct Memory Access,直接記憶體存取) ,用來提供在外設和存儲器之間或者存儲器和存儲器之間的高速資料傳輸。無需CPU幹預,節省CPU資源;ADC轉換出來的值直接指派給定義好的變量中。配置好的DMA可以不停地将ADC轉換值寫到該變量中,在主函數直接判斷該變量就知道此時的AD值,也就是說在主函數中不需要調用ADC_GetConversionValue()函數來擷取轉換值。
DMA跟其他外設一樣需要進行配置通道,使能時鐘等參數。
下面直接看代碼分析:
violate uint32 ADCConvertedValue[10][3];//用來存放ADC轉換結果,也是DMA的目标位址,3通道,每通道采集10次後面取平均數
void DMA_Init(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//使能時鐘
DMA_DeInit(DMA1_Channel1); //将通道一寄存器設為預設值
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR);//該參數用以定義DMA外設基位址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&ADCConvertedValue;//該參數用以定義DMA記憶體基位址(轉換結果儲存的位址)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//該參數規定了外設是作為資料傳輸的目的地還是來源,此處是作為來源
DMA_InitStructure.DMA_BufferSize = 3*10;//定義指定DMA通道的DMA緩存的大小,機關為資料機關。這裡也就是ADCConvertedValue的大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//設定外設位址寄存器遞增與否,此處設為不變 Disable
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//用來設定記憶體位址寄存器遞增與否,此處設為遞增,Enable
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//資料寬度為16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//資料寬度為16位
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //工作在循環緩存模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High;//DMA通道擁有高優先級 分别4個等級 低、中、高、非常高
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//使能DMA通道的記憶體到記憶體傳輸
DMA_Init(DMA1_Channel1, &DMA_InitStructure);//根據DMA_InitStruct中指定的參數初始化DMA的通道
DMA_Cmd(DMA1_Channel1, ENABLE);//啟動DMA通道一
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
下面是ADC的初始化,可以将它與上面的對比一下有啥不同,重複的就不解析了
void Adc_Init(void)
{
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1|RCC_APB2Periph_GPIOA,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = ENABLE;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //連續轉換
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 3;
ADC_Init(ADC1, &ADC_InitStructure);
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_239Cycles5);//通道一轉換結果儲存到ADCConvertedValue[0~10][0]
ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_239Cycles5););//通道二轉換結果儲存到ADCConvertedValue[0~10][1]
ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_239Cycles5); );//通道三轉換結果儲存到ADCConvertedValue[0~10][2]
ADC_DMACmd(ADC1, ENABLE);//開啟ADC的DMA支援
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
做完這兩步,ADCConvertedValue數組的值就會随輸入的模拟電壓改變而改變,在主函數中最好取多幾次的平均值,再通過公式換算成電壓機關。下面是主函數:
int main(void)
{
int sum;
u8 i,j;
float ADC_Value[3];//用來儲存經過轉換得到的電壓值
ADC_Init();
DMA_Init();
ADC_SoftwareStartConvCmd(ADC1, ENABLE);//開始采集
while(1)
{
for(i=0;i<3;i<++)
{
sum=0;
for(j=0;j<10;j++)
{
sum+=ADCConvertedValue[j][i];
}
ADC_Value[i]=(float)sum/(10*4096)*3.3;//求平均值并轉換成電壓值
//列印(略)
}
//延時(略)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ADCConvertedValue的定義用了violate修飾詞,因為這樣可以保證每次的讀取都是從絕對位址讀出來的值,不會因為被會編譯器進行優化導緻讀取到的值不是實時的AD值。
最後提醒一下,接線測試的時候記得接上基準電壓,就是VREF+和VREF-這兩個引腳。如果不想外接線測試就将内部通道的電壓讀出來,這樣就不用配置IO口了。
水準有限,僅供參考,錯誤之處以及不足之處還望多多指教。
---------------------
作者:蝸牛kkk
來源:CSDN
原文:https://blog.csdn.net/weixin_42653531/article/details/81123770
版權聲明:本文為部落客原創文章,轉載請附上博文連結!