1. 程式人生 > 實用技巧 >STM32F4時鐘觸發ADC雙通道取樣DMA傳輸進行FFT+測頻率+取樣頻率可變+顯示波形(詳細解讀)

STM32F4時鐘觸發ADC雙通道取樣DMA傳輸進行FFT+測頻率+取樣頻率可變+顯示波形(詳細解讀)

此文轉載自:https://blog.csdn.net/qq_45620831/article/details/110819495

寫在前面的婆婆媽媽的話

本人大三,參加過數次電賽,來CSDN好久, 每次都是在絕望中從這裡找到了希望,每次都彷彿一個即將被怪獸打翻的小船突然被危險流浪者救起來。是眾多前輩的智慧,讓我有信心繼續做下去,今天上午學校自己舉辦的電子設計競賽公佈了結果,獲了一等獎,萬分開心時,卻也不忘CSDN的恩澤,就有了把自己的東西分享出去的念頭,我希望我寫的這一片博文,可以給需要的人帶來哪怕微小的一點作用。第一次寫,還請包涵。

工程簡介

使用STM32F4系列微控制器(本次使用的是STM32F429,此程式F4全系列使用,只需注意修改好主頻就行了)加陶晶馳3.5寸T0系列串列埠屏,由觸控式螢幕上的按鍵開啟測量,然後顯示訊號峰峰值,頻率,畫出波形,判斷波形。對頻率變化的訊號測量頻率後確定時鐘觸發頻率,即確定了取樣率,用ADC雙通道測量兩路訊號,用DMA傳輸至一個數組記憶體中,然後顯示波形、計算Vpp、並對資料進行FFT,分析頻譜確定波形名稱(可判斷正弦波,三角波,方波,脈衝波(有誤差),鋸齒波,等幅DTMF)

問題分析

用微控制器自帶的ADC對訊號進行取樣時,經常會碰到訊號幅度太小或者太大的問題,這個很好解決,用一個自動增益控制的電路的電路即可解決。(點選連結至自動增益電路篇:)
但是對於一個頻率變化範圍較大的訊號,若是用固定的頻率去取樣,首先,對於時域上,取樣率可能過低導致波形失真,頻譜發生混疊,過高,佔用較大儲存記憶體,難以儲存較多週期的波形,進行FFT後,導致頻率解析度過低。
所以對一個規則訊號,如正弦波,方波,三角波等,要先確定其頻率,(1-500kHz可測)這個頻率運用MCU的輸入捕獲功能,可以測量到非常精準的程度,對一個不規則訊號,如DTMF,可以大致獲得其頻率。這樣就能在有限取樣點數下獲得較好的頻率解析度了。

輸入捕獲測頻率

將一個規則訊號送進一個輸入捕獲管腳,規則訊號處理好幅度後可以直接送進IO口,實測不會影響捕獲,當然也可以選擇將訊號送進一個過零比較器,比較出方波後輸出一個TTL電平送給微控制器,更為穩妥準確。
話不多說,上程式碼:

TIM_HandleTypeDef TIM5_Handler; 
//定時器5控制代碼 8990 
//定時器5通道1輸入捕獲配置  
//arr:自動重灌值(TIM2,TIM5是32位的!!)  
//psc:時鐘預分頻數 
void TIM5_CH1_Cap_Init(__IO uint32_t arr,__IO uint16_t psc) 
 { 
   TIM_IC_InitTypeDef TIM5_CH1Config;
TIM5_Handler.Instance=TIM5; //通用定時器5 TIM5_Handler.Init.Prescaler=psc; //分頻係數 TIM5_Handler.Init.CounterMode=TIM_COUNTERMODE_UP; //向上計數器 TIM5_Handler.Init.Period=arr; //自動裝載值 TIM5_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1; //時鐘分頻因子 HAL_TIM_IC_Init(&TIM5_Handler); //初始化輸入捕獲時基引數 TIM5_CH1Config.ICPolarity=TIM_ICPOLARITY_RISING; //上升沿捕獲 TIM5_CH1Config.ICSelection=TIM_ICSELECTION_DIRECTTI; //對映到TI1上 TIM5_CH1Config.ICPrescaler=TIM_ICPSC_DIV1; //配置輸入分頻,不分頻 TIM5_CH1Config.ICFilter=0110; //配置輸入濾波器,濾波後更穩定 HAL_TIM_IC_ConfigChannel(&TIM5_Handler,&TIM5_CH1Config,TIM_CHANNEL_1); //配置TIM5通道1 HAL_TIM_IC_Start_IT(&TIM5_Handler,TIM_CHANNEL_1); //開啟TIM5的捕獲通道1,並且開啟捕獲中斷 __HAL_TIM_ENABLE_IT(&TIM5_Handler,TIM_IT_UPDATE); //使能更新中斷 } void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim) { GPIO_InitTypeDef GPIO_Initure; __HAL_RCC_TIM5_CLK_ENABLE(); //使能TIM5時鐘 __HAL_RCC_GPIOA_CLK_ENABLE(); //開啟GPIOA時鐘 GPIO_Initure.Pin=GPIO_PIN_0; //PA0 GPIO_Initure.Mode=GPIO_MODE_AF_PP; //複用推輓輸出 GPIO_Initure.Pull=GPIO_PULLDOWN; //下拉 GPIO_Initure.Speed=GPIO_SPEED_FAST; //快速 GPIO_Initure.Alternate=GPIO_AF2_TIM5; //PA0複用為TIM5通道1 HAL_GPIO_Init(GPIOA,&GPIO_Initure); HAL_NVIC_SetPriority(TIM5_IRQn,2,0); //設定中斷優先順序,搶佔優先順序2,子優先順序0 HAL_NVIC_EnableIRQ(TIM5_IRQn); //開啟ITM5中斷通道 }

基礎的配置,註釋都已經說明,中斷優先順序設定較低,影響不大,因為我每次測完後都關閉,再次迴圈時再開啟。
中斷服務函式因篇幅有限未放出,可以私信聯絡我發完整程式碼。

觸發ADC的時鐘配置

 TIM_HandleTypeDef htim3; 

 /* TIM3 init function */ 
 void MX_TIM3_Init(void) 
 { 
TIM_ClockConfigTypeDef sClockSourceConfig; 
 TIM_MasterConfigTypeDef sMasterConfig; 

 htim3.Instance = TIM3; 
 htim3.Init.Prescaler =89-1; //1MHz頻率 
 htim3.Init.CounterMode = TIM_COUNTERMODE_UP; 
 htim3.Init.Period =250-1; //預設時鐘觸發頻率,此時AD取樣率為4k 
 htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; //不分頻
 HAL_TIM_Base_Init(&htim3); 

 sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;  //選擇內部時鐘
 HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig); 

 sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; //更新觸發
 sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; 
 HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig); 

 } 

 void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base) 
 { 

 if(htim_base->Instance==TIM3) 
 { 
 /* Peripheral clock enable */ 
 __HAL_RCC_TIM3_CLK_ENABLE(); 
 } 
 }
    void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* htim_base) 
 { 
 if(htim_base->Instance==TIM3) 
 {
 /* Peripheral clock disable */
 __HAL_RCC_TIM3_CLK_DISABLE(); 
 } 
 }

這裡需要注意的是預分頻係數,和自動重灌載值的設定,觸發AD取樣的頻率為90M/(Prescaler*Period)90M是TIM3的時鐘頻率,預分頻係數Prescaler建議固定不動,每次通過修改period來改變觸發頻率。由於程式碼篇幅實在過大,僅介紹關鍵部分。

ADC+DMA配置

 void MX_ADC1_Init(void) 
 { 
 ADC_ChannelConfTypeDef sConfig; 

 /**Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) 
 */ 
 hadc1.Instance = ADC1; 
 hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; //90M/4=22.5M 超過36M準確度會下降 
 hadc1.Init.Resolution = ADC_RESOLUTION_12B; 
 hadc1.Init.ScanConvMode = ENABLE; 
 hadc1.Init.ContinuousConvMode = DISABLE; 
 hadc1.Init.DiscontinuousConvMode = DISABLE; //觸發單次轉換,故設定為DISABLE 
 hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; 
 hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO; 
 hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; 
 hadc1.Init.NbrOfConversion = 2; 
 hadc1.Init.DMAContinuousRequests = ENABLE; 
 hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; 
 HAL_ADC_Init(&hadc1); 

 /**Configure for the selected ADC regular channel its corresponding rank in the sequencer and its sample time. 
 */ 
 sConfig.Channel = ADC_CHANNEL_5; //先採5通道,再採6通道 
 sConfig.Rank = 1; 
 sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES; //15個取樣週期 Tconv=28+12週期 以滿足最高400K取樣率 
 HAL_ADC_ConfigChannel(&hadc1, &sConfig);
 sConfig.Channel = ADC_CHANNEL_6; 
 sConfig.Rank = 2; 
 sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES; 

 HAL_ADC_ConfigChannel(&hadc1, &sConfig); 

 } 
 void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) 
 { 

 GPIO_InitTypeDef GPIO_InitStruct; 
 if(hadc->Instance==ADC1) 
 { 
 /* USER CODE BEGIN ADC1_MspInit 0 */ 

 /* USER CODE END ADC1_MspInit 0 */ 
 /* Peripheral clock enable */ 
 __HAL_RCC_ADC1_CLK_ENABLE(); 

 /**ADC1 GPIO Configuration 
 PA5 ------> ADC1_IN5 
 PA6 ------> ADC1_IN6 
 */ 
 GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6; //A線時鐘去MX_GPIO_Init開啟 
 GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; 
 GPIO_InitStruct.Pull = GPIO_NOPULL; 
 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); 

 /* ADC1 DMA Init */ 
 /* ADC1 Init */ 
 hdma_adc1.Instance = DMA2_Stream0; 
 hdma_adc1.Init.Channel = DMA_CHANNEL_0; 
 hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; //傳輸方向為外設到記憶體
 hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; //外設只有一個ADC,所以不遞增
 hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;  //儲存地址要遞增
 hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;  //每次傳輸半字即可,即16位
 hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; 
 hdma_adc1.Init.Mode = DMA_CIRCULAR; //開啟迴圈傳輸
 hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH; 
 hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE; 

 HAL_DMA_Init(&hdma_adc1); 
 
 
 __HAL_LINKDMA(hadc,DMA_Handle,hdma_adc1); //把DMA和ADC連結起來,這樣ADC每轉換完一個數據,就觸發DMA傳輸。

需要注意的是觸發選擇外部觸發,雙通道轉換要設定掃描模式使能,因為是用時鐘觸發,所以關閉連續轉換。雙通道所以NbrOfConversion設定為2,開啟DMA請求。單次轉換完成觸發。
設定轉換時間的時候要注意,轉換時間越長越精確,但是每觸發一次要進行兩個通道的轉換,這兩個通道的轉換時間之和一定要小於時鐘觸發的間隔。
關於DMA的傳輸開始和停止問題,有一個函式可以同時開啟ADC和DMA傳輸中斷:
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&ADC_DMA_ConvertedValue,8192); 意思就是開啟轉換和傳輸,把ADC1的資料傳輸到ADC_DMA_ConvertedValue這個數組裡,注意使用這個函式時,一定要加強制轉換符(uint32_t*),這是HAL庫自己定義的,即使我們定義的陣列為16位。
傳輸完8192個數據停止DMA傳輸並進入中斷,這個HAL庫裡有一個專門的中斷函式:
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) 在中斷裡面做自己想做的事情就可以了。

main函式,整體思想的體現

放程式碼在這感覺很累贅,暫時不放了,直接說設計思想:

  1. 在ADC_DMA傳輸完成中斷裡設定一個完成標誌,每次傳輸完把這個標誌置為一,然後在main函式的while(1)裡迴圈檢測,檢測到之後就進行資料處理,處理完再把標誌設定為0。
  2. UART用於接受和傳送資料給串列埠屏,同樣設定接受中斷標誌,用來檢測串列埠屏上的按鍵。
  3. 通過捕獲獲取訊號頻率,設定自動重灌載值,(設定多少可根據自己的需求定),開啟TIM3時鐘,觸發AD轉換。
  4. 資料採集完成後顯示波形,計算峰峰值,呼叫DSP庫進行FFT,得到頻率上的資訊。
  5. 波形判斷是利用訊號二次諧波和基波分量之比,利用各個波形的比值不同去判斷波形,每種波形的具體比值可以用示波器測量開啟FFT測算。
    **總結:**其實對於這類專案,最重要的是如何把資料按自己想要的形式採集並放進所建立的陣列中,關鍵就是取樣率的設定,因為這個直接關係到FFT後的精度問題。資料採集到了,怎麼去處理,那就是隨心所欲了,就可以盡情發揮自己的數學天賦,從這若干個資料中獲得自己想要的資訊。

晒幾張測試圖




需要完整程式碼的私信我。
有問題的也可以留言,看到都會給解答。
創作不易,覺得有幫助的夥伴點個贊好不好,您的點贊是我繼續創作的動力。
謝謝朋友們!