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后的精度問題。數據采集到了,怎么去處理,那就是隨心所欲了,就可以盡情發揮自己的數學天賦,從這若干個數據中獲得自己想要的信息。

曬幾張測試圖

正弦波
方波三角波
在這里插入圖片描述

心電波形
需要完整代碼的私信我。
有問題的也可以留言,看到都會給解答。
創作不易,覺得有幫助的伙伴點個贊好不好,您的點贊是我繼續創作的動力。
謝謝朋友們!
在這里插入圖片描述


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM