STM32筆記(23):使用 ADC 及 DMA 多通道讀取電壓值(下)


[DMA 設定步驟]

要編寫 DMA 程式,可按照如下步驟進行設定:
(1) 啟動 ADC1 及 GPIOA 的 RCC 時鐘
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); //啟用ADC1和GPIOA時鐘
(2) 設定 GPIO Pin 並初始化 GPIOA
GPIO_InitTypeDef GPIO_InitStructure;	//定義 GPIO_InitTypeDef 類型的結構體 
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 ; // 啟用PA0-PA2當做類比輸入 
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;	//模擬輸入引腳
GPIO_Init(GPIOA, &GPIO_InitStructure);		//輸入時不用設置速率
相關 GPIO 相關設定說明,請參考:STM32筆記(17):按鍵控制-使用 GPIO 讀取

(3) 啟用 DMA1 和 ADC1 的 RCC 時鐘
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);  	//啟用DMA時鐘
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);  	//啟用ADC1時鐘
(4) 設定 DMA 通道
DMA_InitTypeDef DMA_InitStructure;
	
DMA_DeInit(DMA1_Channel1);		//恢復 DMA1頻道初始值
DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address;	 	//ADC地址
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&ADC_ConvertedValue;//記憶體位址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;				//外設為資料來源
DMA_InitStructure.DMA_BufferSize = Sample_Num*Channel_Num;		//保存DMA要傳輸的資料總大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外設地址固定
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  		//記憶體位址自增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;	//設定外設資料寬度為半字16位元
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;	//設定記憶體資料寬度為半字16位元
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;					//DMA_Mode_Circular;	
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;					//禁止記憶體到記憶體的傳輸
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel1, ENABLE); //啟用 DMA channel1 
(5) 設定 ADC1
ADC_InitTypeDef ADC_InitStructure;	
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;	//獨立ADC模式
ADC_InitStructure.ADC_ScanConvMode = ENABLE ; 	 	//掃描模式用於多通道採集
	
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;	//開啟連續轉換模式,即不停地進行ADC轉換
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//不使用外部觸發轉換
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; 	//採集資料右對齊
ADC_InitStructure.ADC_NbrOfChannel = 3;	 			//要轉換的通道數目1
ADC_Init(ADC1, &ADC_InitStructure);    
(6)設定 ADC 時鐘及通道
RCC_ADCCLKConfig(RCC_PCLK2_Div6);	// 	設定ADC時鐘,為PCLK2的8分頻,即9MHz
	
/*設定 ADC1 的通道0-2為239.5個採樣週期,序列為1 */ 
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5 );	
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5 );	
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_239Cycles5 );	
(7)設定 ADC 時鐘及通道
ADC_DMACmd(ADC1, ENABLE);	//啟動 ADC1 DMA
ADC_Cmd(ADC1, ENABLE);		//啟動 ADC1
(8)對 ADC 進行校準
ADC_ResetCalibration(ADC1);	 //復位校準暫存器  
while(ADC_GetResetCalibrationStatus(ADC1));	//等待校準暫存器復位完成
ADC_StartCalibration(ADC1);					//ADC校準 
while(ADC_GetCalibrationStatus(ADC1));		//等待校準完成
依照上述幾個步驟,就可完成定 DMA 的控制,以下就用 DMA 的三個通道,紀錄三個 GPIO Pin 電壓的量測值,每個 GPIO Pin 接一個可變電阻,來改變電壓值,分別顯示三個電壓值在 OLED 上。

[材料]

  • STM32F103C8T6 主板 x 1
  • OLED SSD1306 顯示器 x 1
  • STLINK V2 模擬下載器 x 1
  • 可變電阻 x 3
  • 麵包板 x 1
  • 連接線 x N 條

[接線與電路圖]

三個可變電阻左右兩側分別接 3.3v 及 GND,中間的接腳分別接在 STM32F103x 的 PA0、PA1 及 PA2,與 SSD1306 連接的方式如下:
STM32F103xSSD1306 OLED
3.3vVDD
GNDGND
B8SCK/SCL
B9SDA


[程式]

主程式一開始先對 OLED 進行初始化,進入迴圈循環後就改變 CCR 值,等中斷產生,主程式 main.c 如下:
#include "stm32f10x.h"
#include "delay.h"
#include "oled.h"
#include "bmp.h"
#include "adcx.h"

float ADValue[Channel_Num];

int main (void)
{
	uint16_t vtg;
	
	delay_init();	   	//延時函數初始化	  	
	OLED_Init();		//初始化OLED  
	ADC_GPIO_Config();
	ADC_Mode_Config();
	
	OLED_Clear(0);     	//清除螢幕
	OLED_ShowString(1, 2, "Voltage1:",16);
	OLED_ShowString(1, 4, "Voltage2:",16);
	OLED_ShowString(1, 6, "Voltage3:",16);
	
	while (1)
	{
		u16 x;
		u16 colnum;
		for(x=0;x<3;x++){

			ADValue[x] = Read_ADC_AverageValue(x);
		
			colnum = 2+(x*2);
			OLED_ShowNum(72, colnum, ADValue[x] / 4096 * 3.3, 1, 16);	  //整數位
			OLED_ShowString(82, colnum, ".",16); 

			vtg = (uint16_t)(ADValue[x] / 4096 * 3.3 * 100) % 100;  //小數點後兩位
			if (vtg < 10) {
				OLED_ShowNum(90, colnum, vtg, 2, 16);	
				OLED_ShowString(89, colnum,"0",16); 		//個位數時前面補0	
			} else
			{
				OLED_ShowNum(90, colnum, vtg, 2, 16);			
			}
		
			delay_ms(300);
		}	
	}
}
adcx.c 程式如下:
#include "stm32f10x.h" 
#include "delay.h"
#include "adcx.h"

#define ADC1_DR_Address    ((u32)0x40012400+0x4c)
 
__IO uint16_t ADC_ConvertedValue[Sample_Num][Channel_Num];
u16 AD_After_Filter[Channel_Num]; 

// 初始化 GPIO
void ADC_GPIO_Config(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); //啟用ADC1和GPIOA時鐘
//	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //啟用ADC1和GPIOA時鐘

	GPIO_InitTypeDef GPIO_InitStructure;	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 ; // 啟用PA0-PA2當做類比輸入 
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;	//模擬輸入引腳
	GPIO_Init(GPIOA, &GPIO_InitStructure);			//輸入時不用設置速率
}

// 設定 ADC1 的工作模式為 MDA 模式
void ADC_Mode_Config(void)
{
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);  	//啟用DMA時鐘
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);  	//啟用ADC1時鐘
	
	/* DMA channel1 configuration */
	DMA_InitTypeDef DMA_InitStructure;
	
	DMA_DeInit(DMA1_Channel1);		//DMA1的通道1
	DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address;	 	//ADC地址
	DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&ADC_ConvertedValue;//記憶體位址
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;				//外設為資料來源
	DMA_InitStructure.DMA_BufferSize = Sample_Num*Channel_Num;		//保存DMA要傳輸的資料總大小
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外設地址固定
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  		//記憶體位址自增
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;	//設定外設資料寬度為半字16位元
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;	//設定記憶體資料寬度為半字16位元
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;					//DMA_Mode_Circular;	
	DMA_InitStructure.DMA_Priority = DMA_Priority_High;
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;					//禁止記憶體到記憶體的傳輸
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);
	DMA_Cmd(DMA1_Channel1, ENABLE); 	//啟用 DMA channel1 
	
	/* ADC1 configuration */
	ADC_InitTypeDef ADC_InitStructure;	
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;	//獨立ADC模式
	ADC_InitStructure.ADC_ScanConvMode = ENABLE ; 	 	//掃描模式用於多通道採集
	
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;	//開啟連續轉換模式,即不停地進行ADC轉換
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//不使用外部觸發轉換
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; 	//採集資料右對齊
	ADC_InitStructure.ADC_NbrOfChannel = 3;	 			//要轉換的通道數目1
	ADC_Init(ADC1, &ADC_InitStructure);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);	// 	設定ADC時鐘,為PCLK2的8分頻,即9MHz
	
	/*配置ADC1的通道0-2為239.5個採樣週期,序列為1 */ 
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5 );	
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5 );	
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_239Cycles5 );	
	
	ADC_DMACmd(ADC1, ENABLE);	//啟動 ADC1 DMA
	ADC_Cmd(ADC1, ENABLE);		//啟動 ADC1
	
	ADC_ResetCalibration(ADC1);					//復位校準寄存器  
	while(ADC_GetResetCalibrationStatus(ADC1));	//等待校準寄存器復位完成
	ADC_StartCalibration(ADC1);					//ADC校準 
	while(ADC_GetCalibrationStatus(ADC1));	//等待校準完成
	 
	//ADC_SoftwareStartConvCmd(ADC1, ENABLE);	// 由於沒有採用外部觸發,所以使用軟體觸發ADC轉換
}
 

u16 Read_ADC_AverageValue(u16 Channel)
{
	u8 t;
	u32 sum = 0;
	
	//完成一次DMA傳輸,資料大小10*5	
	DMA_SetCurrDataCounter(DMA1_Channel1,Sample_Num*Channel_Num);				//設置DMA的傳送數量為10*5
	DMA_Cmd(DMA1_Channel1,ENABLE);
	
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);					//使用軟體轉換啟動功能	
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1)!=SET);			//等待DMA傳送完成
	DMA_ClearFlag(DMA1_FLAG_TC1);							//清除DMA傳送完成標誌
	DMA_Cmd(DMA1_Channel1,DISABLE);	
	ADC_SoftwareStartConvCmd(ADC1, DISABLE);	

	for(t=0;t<Sample_Num;t++)
		{
			sum += ADC_ConvertedValue[t][Channel];
		}
	AD_After_Filter[Channel] = sum/Sample_Num;
	sum = 0;
	return AD_After_Filter[Channel];
	//delay_ms(5);
}

adcx.h 程式如下:
#ifndef __ADCX_H
#define __ADCX_H
#include "stm32f10x.h"
 
#define Sample_Num 10
#define Channel_Num 3

extern __IO uint16_t ADC_ConvertedValue[Sample_Num][Channel_Num];
extern u16 AD_After_Filter[Channel_Num];

void ADCX_Init(void);
uint16_t ADCX_GetValue(u8 ch);

u16 Get_Temp(void);
u16 Get_Adc_Average(u8 ch,u8 times);
short Get_Temprate(void);

void ADC_GPIO_Config(void);	
void ADC_Mode_Config(void); 
	
u16 Read_ADC_AverageValue(u16 Channel);

#endif

完整的程式請參考 Github:9.Using ADC DMA Read Voltage

[結果]

轉動可變電阻時,GPIO 的電壓值隨著改變,值介於 0~3.3v 之間。各位有沒有發現,我的可變電阻從左至右,分別接在 PA0, PA2 及 PA3,對應設定的通道從 0~2,可是影片中,旋轉中間接到 PA1 的可變電阻,竟然是 Voltage3 的值改變,我還沒找到在哪裡出問題,有空在回來除錯。

[參考資料]



1 留言

  1. 不好意思,請問可以教學使用Timer觸發雙ADC同步規則採樣(Dual regular simultaneous mode)嗎,目前遇到一個問題是ADC Sampling Time與Timer觸發採樣頻率的計算,當我想使Tconv(Sampling Time+Conversion Time)<(1/Timer採樣頻率)時,卻只能採樣第一次便停止了,反而是當Tconv>(1/Timer採樣頻率)時可以持續採樣轉換。

    回覆刪除

張貼留言

較新的 較舊