stm32之ADC應用實例(單通道、多通道、基於DMA)

文本僅作記錄。。數組

  • 硬件:STM32F103VCT6
  • 開發工具:Keil uVision4
  • 下載調試工具:ARM仿真器

網上資料不少,這裏作一個詳細的整合。(也不是很詳細,但很通俗)。 
所用的芯片內嵌3個12位的模擬/數字轉換器(ADC),每一個ADC共用多達16個外部通道,2個內部通道。緩存

3個:表明ADC一、ADC二、ADC3(下圖是芯片固件庫的截圖) 
 
12位:也叫ADC分辨率、採樣精度。先來看看二進制的12位可表示0-4095個數,也就是說轉換器經過採集轉換所獲得的最大值是4095,如:「111111111111」=4095,那麼咱們怎麼經過轉換器轉換出來的值獲得實際的電壓值呢?若是咱們要轉換的電壓範圍是0v-3.3v的話,轉換器就會把0v-3.3v平均分紅4096份。設轉換器所獲得的值爲x,所求電壓值爲y。 
那麼就有: 
 
16個外部通道:簡單的說就是芯片上有16個引腳是能夠接到模擬電壓上進行電壓值檢測的。16個通道不是獨立的分配給3個轉換器(ADC一、ADC二、ADC3)使用,有些通道是被多個轉換器共用的。首先看看16個通道在固件庫的宏定義(寫代碼要看的): 
 
到這裏你們可能會有疑問,每一個通道到底對應哪一個引腳呢?下面先給出部分引腳圖: 
 
16個通道的引腳都在上面的圖中,拿其中的一個進行說明:函數

ADC123_IN10字母「ADC」不用多說,「123」表明它被3個(ADC一、ADC二、ADC3)轉換器共用的引腳,「10」對應剛纔那張宏定義圖裏面的ADC_Channel_10,這樣就能找到每一個通道對應的引腳了。工具

2個內部通道:一個是內部溫度傳感器,一個是內部參考電壓。開發工具


在某個項目中要用到芯片裏面的AD轉換器,那麼要怎麼寫應用代碼?(如下是代碼講解)測試

芯片固件的庫函數爲咱們提供了不少封裝好的函數,只要運用它提供的函數接口就能夠了,宏觀上來說就搞懂兩個事情就好了:優化

  • 初始化(設置用的哪一個引腳、單通道、仍是多通道同時轉換、是否使用DMA等配置)?
  • 怎麼讓轉換器進行一次數據獲取?

如下分別講述三種不一樣方式(單通道、多通道、基於DMA的多通道採集)的ADC應用實例:ui

/*單通道的ADC採集*/es5

void  Adc_Config(void)調試

{  

    /*定義兩個初始化要用的結構體,下面給每一個結構體成員賦值*/

    ADC_InitTypeDef ADC_InitStructure;

    GPIO_InitTypeDef GPIO_InitStructure;

 

    /*

      使能GPIOA和ADC1通道時鐘

      注意:除了RCC_APB2PeriphClockCmd還有RCC_APB1PeriphClockCmd,那麼該如何選擇?

      APB2:高速時鐘,最高72MHz,主要負責AD輸入,I/O,串口1,高級定時器TIM

      APB1:低速時鐘,最高36MHz,主要負責DA輸出,串口二、三、四、5,普通定時器TIM,USB,IIC,CAN,SPI

    */

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1, ENABLE );  

    RCC_ADCCLKConfig(RCC_PCLK2_Div6);  //72M/6=12, ADC的採樣時鐘最快14MHz 

 

    /*配置輸入電壓所用的PA0引腳*/        

    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轉換器,但ADC轉換器並不知道你用的是哪一個引腳吧?

     這一步目的是:設置指定ADC的規則組通道(引腳),設置它們的轉化順序和採樣時間

     函數原型:void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, u8 ADC_Channel, u8 Rank, u8 ADC_SampleTime)

     參數1 ADCx:x能夠是1或者2來選擇ADC外設ADC1或ADC2

     參數2 ADC_Channel:被設置的ADC通道  範圍ADC_Channel_0~ADC_Channel_17

     參數3 Rank:規則組採樣順序。取值範圍1到16。

     ADC_SampleTime:指定ADC通道的採樣時間值  (取值範圍?請看下面的附錄圖4)

    */ 

     ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5 );     

 

    ADC_Cmd(ADC1, ENABLE);  //使能指定的ADC  注意:函數ADC_Cmd只能在其餘ADC設置函數以後被調用

 

    /*下面4步按流程走,走完就行*/

    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-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); 

 

配置多一個IO(PA1)口, 也就是通道1。

2.在void Adc_Config(void)函數裏面添加:

ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5 );  

 

先不指定ADC轉換通道。

3.在主函數循環裏改成:

    while(1)

    {

      /*先採集通道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); 

 

      /*再採集通道2數據*/

      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); 

 

      //加入適當延時

    }

 

完成以上三步就能把單通道擴展到雙通道(或者更多個通道)。不過還有一種基於DMA的多通道轉換更加合適。


首先簡單介紹DMA,DMA(Direct Memory Access,直接內存存取) ,用來提供在外設和存儲器之間或者存儲器和存儲器之間的高速數據傳輸。無需CPU干預,節省CPU資源;ADC轉換出來的值直接賦值給定義好的變量中。配置好的DMA能夠不停地將ADC轉換值寫到該變量中,在主函數直接判斷該變量就知道此時的AD值,也就是說在主函數中不須要調用ADC_GetConversionValue()函數來獲取轉換值。

DMA跟其餘外設同樣須要進行配置通道,使能時鐘等參數。 
下面直接看代碼分析:

/*基於DMA的ADC多通道採集*/

 

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通道一

}

下面是ADC的初始化,能夠將它與上面的對比一下有啥不一樣,重複的就不解析了

void Adc_Init(void)

{

    ADC_InitTypeDef ADC_InitStructure;

    GPIO_InitTypeDef GPIO_InitStructure;

    /*3個IO口的配置(PA0、PA一、PA2)*/

    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);

    /*IO和ADC使能時鐘*/

    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));

 

}

作完這兩步,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;//求平均值並轉換成電壓值

            //打印(略)

        }

        //延時(略)

    }

}

ADCConvertedValue的定義用了violate修飾詞,由於這樣能夠保證每次的讀取都是從絕對地址讀出來的值,不會由於被會編譯器進行優化致使讀取到的值不是實時的AD值。

最後提醒一下,接線測試的時候記得接上基準電壓,就是VREF+和VREF-這兩個引腳。若是不想外接線測試就將內部通道的電壓讀出來,這樣就不用配置IO口了。

水平有限,僅供參考,錯誤之處以及不足之處還望多多指教。

相關文章
相關標籤/搜索