基于STM32G031的失真度测试仪(CubeMX+ADC+DMA+OLED+EC11)
目錄
- 項目介紹
- 硬件介紹
- 設計思路
- 各功能代碼及說明
- SPWM波生成
- ADC采樣
- FFT
- 獲取按鍵動作
- OLED顯示
- 系統頂層
- 功能展示
- OLED顯示采樣波形
- OLED顯示頻譜/失真度曲線
- 項目總結
👉工程文件及代碼:參見【2022寒假在家練】基于STM32G031的失真度測試儀
👉 CSDN:工程源代碼下載
👉 Github-KafCoppelia/DistortionMeter
項目介紹
本項目基于電子森林的STM32G031口袋儀器訓練平臺,基于CubeMX與Keil,實現了:
硬件介紹
👉 電子森林-基于STM32的簡易示波器/頻譜儀/信號發生器學習平臺
一個蜂鳴器用于音效輸出;
基于STM32G031的測試測量學習套件的構成框圖如上圖所示。
設計思路
設計的整體結構框圖如下圖所示:
👉 整體結構參考:SCOPE-F072–基于STM32F072的多功能掌中儀器
由于沒有上操作系統,初始化外設及OLED后,整體為一個循環,判斷當前系統處于示波器或頻譜/失真度狀態(即OLED顯示的內容是什么),再各判斷是否為頁初始化(初次進入該狀態時會設置狀態,之后相互切換就不會重置狀態位),之后執行對應的操作。按鍵、旋鈕的交互功能如上圖箭頭所示。
各功能代碼及說明
該開發板電路圖如圖所示:
板卡左下角PB0產生PWM,可通過示波器測PWM測試點調試,連接左上角JP1的1、2(實物排針右兩個),通過對ADC_M(PA0)做采樣即可獲取波形數據。
SPWM波生成
PWM信號源相關代碼參見source.c/.h
SPWM波主要是調節一般PWM波的占空比,使輸出波所占面積和對應正弦波面積相等。所以首先需要一組正弦波數據,可以通過Python等方式計算:
import numpy as npdef sin_wave(point, num):y = []for i in range(0, point):fz = num/2 * np.sin(np.pi/point*2*i) + num/2y.append(fz)return yif __name__ == "__main__":y = sin_wave(256, 256)print(y)y2 = []for dot in y:y2.append(round(dot))print(y2)其中,point為生成數據的點數,如128、256個;num為生成數據的范圍,表示從0~num。生成的數據可以static const uint16_t儲存:
#define SIGNAL_LENGTH 256 static const uint16_t sine_table[SIGNAL_LENGTH] = {128, 131, 134, 137, 141, 144, 147, 150, 153, 156, 159, 162, 165, 168, 171, 174, 177, 180, 183, 186, 188, 191, 194, 196, 199, 202, 204, 207, 209, 212, 214, 216, 219, 221, 223, 225, 227, 229, 231, 233, 234, 236, 238, 239, 241, 242, 244, 245, 246, 247, 249, 250, 250, 251, 252, 253, 254, 254, 255, 255, 255, 256, 256, 256,256, 256, 256, 256, 255, 255, 255, 254, 254, 253, 252, 251, 250, 250, 249, 247, 246, 245, 244, 242, 241, 239, 238, 236, 234, 233, 231, 229, 227, 225, 223, 221, 219, 216, 214, 212, 209, 207, 204, 202, 199, 196, 194, 191, 188, 186, 183, 180, 177, 174, 171, 168, 165, 162, 159, 156, 153, 150, 147, 144, 141, 137, 134, 131, 128, 125, 122, 119, 115, 112, 109, 106, 103, 100, 97, 94, 91, 88, 85, 82, 79, 76, 73, 70, 68, 65, 62, 60, 57, 54, 52, 49, 47, 44, 42, 40, 37, 35, 33, 31, 29, 27, 25, 23, 22, 20, 18, 17, 15, 14, 12, 11, 10, 9, 7, 6, 6, 5, 4, 3, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 4, 5, 6, 6, 7, 9, 10, 11, 12, 14, 15, 17, 18, 20, 22, 23, 25, 27, 29, 31, 33, 35, 37, 40, 42, 44, 47, 49, 52, 54, 57, 60, 62, 65, 68, 70, 73, 76, 79, 82, 85, 88, 91, 94, 97, 100, 103, 106, 109, 112, 115, 119, 122, 125 };將TIM3_CH3(PB0)設為PWM輸出,prescaler與counter period暫且不管。從正弦波表至設置頻率的SPWM波及振幅還需變換:
void Generate_Sine(void) {uint16_t tim_period;uint16_t i;if(is_source_on())Sine_Stop();tim_period = 64000000 / SIGNAL_LENGTH / source_signal.frequency; // 25__HAL_TIM_SET_AUTORELOAD(&htim3, tim_period-1);for (i = 0; i < SIGNAL_LENGTH; i++)sine_value[i] = (sine_table[i]-128) * (tim_period-1) / 256 * source_signal.amplitude / 1650 + tim_period/2;if(!is_source_on())Sine_Start(); }根據:
fSPWM=fsine?Nf_{SPWM}=f_{sine}\cdot NfSPWM?=fsine??N
由此算得TIM3的周期,通過__HAL_TIM_SET_AUTORELOAD()設定不同頻率正弦波下的SPWM的頻率。此外,還需對正弦波表做歸一化,將其直流偏置移動到1.65V(IO口輸出最高3.3V),其中source_signal為信號源參數的結構體,儲存源信號的頻率和幅度,開始/關閉產生正弦波調用
HAL_TIM_PWM_Start_DMA()及HAL_TIM_PWM_Stop_DMA()即可。
由于當設置好輸出頻率及振幅后,傳輸至TIM3_CH3的AutoReload值為周期性循環的,因此設置TIM3_CH3的DMA有利于提高CPU的效率:
| Channel | 隨意 |
| Direction | Memory to Peripheral |
| Priority | Medium |
| Mode | Circular |
| Increment Address | Memory |
| Data Width of Peripheral | Word |
| Data Width of Memory | Half Word |
ADC采樣
ADC采樣相關代碼參見sample.c/.h
一些采樣的全局變量,256個采樣值,9檔采樣率,初始設置采樣率下標:
#define SAMPLE_RATES_NUM 9 uint16_t ADC_Value[SAMPLE_POINTS]; static const uint32_t sample_rate_list[SAMPLE_RATES_NUM] = {2560, 5120, 10240, 20480, 40960, 81920, 102400, 204800, 409600}; static int8_t sample_rate_index = 5;ADC部分參數設置如下,其余參數大致選默認的即可:
| Clock Prescaler | /2 |
| Resolution | 12-bit |
| Data Aligment | Right |
| SamplingTime Common 1 | 1.5 |
| SamplingTime Common 2 | 1.5 |
| N of Conversion | 1 |
| External Trigger Conversion Source | Timer 1 Tigger Out Event 2 |
| External Trigger Conversion Edge | Trigger detection on the falling edge |
ADC對PA0的采樣使用TIM1觸發,當TIM1出現下邊沿時,開始或結束采樣,此時TIM1的頻率即為ADC實際采樣率。設置TIM1_CH3 PWM Generation No Output,prescaler與counter period暫且不管,重點設置TRGO Parameters:
| Master/Slave Mode | Disable |
| Trigger Event Selection TRGO | Reset |
| Trigger Event Selection TRGO2 | Update Event |
每次開始采樣前,需要根據采樣值設置TIM1的AutoReload與CCR值(占空比50%即可):
uint32_t Get_SampleRate(void) {return sample_rate_list[sample_rate_index]; } void Set_SampleRate(uint32_t sample_rate) {__HAL_TIM_SET_AUTORELOAD(&htim1, 64000000 / sample_rate - 1);__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, 32000000 / sample_rate); }從ADC采集的數據經過DMA存入數組,DMA設置如下:
| Channel | 隨意 |
| Direction | Memory to Peripheral |
| Priority | High |
| Mode | Normal |
| Increment Address | Memory |
| Data Width of Peripheral | Half Word |
| Data Width of Memory | Half Word |
開啟采樣需要開啟TIM1及ADC的DMA傳輸,注意開始采樣前需要對ADC進行校準:
void Sample_Start(uint16_t *ADCValue) {HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3);HAL_ADCEx_Calibration_Start(&hadc1);HAL_ADC_Start_DMA(&hadc1, (uint32_t *)ADCValue, SAMPLE_POINTS); }void Sample_Stop(void) {HAL_TIM_Base_Stop(&htim1);HAL_TIM_Base_Stop_IT(&htim1);HAL_ADC_Stop_DMA(&hadc1); }FFT
FFT及頻譜相關代碼參見specturm.c/.h及庫`
該部分使用Adafruit_ZeroFFT庫,選定做FFT點數,選擇對應的窗函數,刪去庫多余的代碼節約空間。
👉 Adafruit_ZeroFFT
通過調用ZeroFFT()即可計算FFT:
memcpy(FFT_Value, ADC_Value, sizeof(FFT_Value)); ZeroFFT((int16_t*)FFT_Value, FFT_POINT)原代碼內的FFT最后計算舍去了虛部,只保留了實部,在此參考寒假在家一起練(1) - 有信號發生器功能的簡易示波器的該部分代碼,改為計算幅值譜,并修正直流分量,最后將整個FFT數組做歸一化即可得到歸一化幅值譜:
for (i = 0; i < length; i++) {real = *pOut++;img = *pOut++;*pSrc++ = sqrt((int32_t)real * real + (int32_t)img * img); } source[0] /= 2;通過計算后的頻譜計算中心頻率、獲得某頻率所在頻譜下標調用FFT_BIN、FFT_INDEX即可:
float Get_ActualFreq(uint16_t *FFTValue, uint32_t sample_rate) {return FFT_BIN(Get_SpectrumMax(FFTValue, 1), sample_rate, FFT_POINT); } uint8_t Get_SpectrumMax(uint16_t *FFTValue, uint8_t ignore_dc) {uint8_t i;uint8_t temp_max_index = ignore_dc ? 2 : 0;for (i = ignore_dc ? 3 : 1; i <= FFT_POINT / 2; i++)if (FFTValue[i] > FFTValue[temp_max_index])temp_max_index = i;return temp_max_index; }重點計算THD,需要計算中心頻率功率、高次諧波的功率和,作者計算了一定采樣率下頻譜包含的所有諧波的功率和,當然也可只取N次,但容易出現交互調整采樣率后該諧波不在頻譜內的意外(頻譜所包含的頻率只有Sample Rate/2)。
float Get_THDx(uint16_t *FFTValue, uint8_t ignore_dc, uint32_t sample_rate) {uint16_t maxN_power = 0;uint16_t max_power = 0;int i = 1; max_power = FFT_Value[FFT_INDEX(Get_SourceFreq(), sample_rate, FFT_POINT)];while(Get_SourceFreq()*(i+1) <= sample_rate/2){maxN_power += FFT_Value[FFT_INDEX(Get_SourceFreq()*(i+1), sample_rate, FFT_POINT)];i++;}return sqrtf(((float)maxN_power)/max_power); }最后,由于OLED顯示僅128像素,只能將256點FFT的0~127歸一化后作為數據顯示(顯示區域高90,寬128)。當對數形式顯示時,將0值映射至-30dB,將最大值映射為0,由于算得的FFT數組已為幅度譜,因此只需lg?(x)\lg(x)lg(x)即可,乘-30為改符號為正,并映射至0~90。
void Generate_Spectrum(uint16_t *FFTValue, uint8_t *y, uint8_t log_or_linear) {uint8_t max_index = Get_SpectrumMax(FFTValue, 0);uint8_t i;if(log_or_linear){for(i = 0; i < FFT_POINT / 2; i++){if(FFTValue[i] > 0){y[i] = (uint8_t)(-30*log10f((1.0*FFTValue[i]/FFTValue[max_index])));}else{y[i] = GRAPH_HEIGHT-1;}}}else{for(i = 0; i < FFT_POINT / 2; i++){y[i] = (GRAPH_HEIGHT-1) * (FFTValue[max_index] - FFTValue[i]) / FFTValue[max_index];}} }獲取按鍵動作
按鍵相關代碼參見keys.c/.h
對常規按鍵的處理參考SCOPE-F072–基于STM32F072的多功能掌中儀器中對按鍵的處理,通過設置TIM14每1ms產生Update中斷,并對按鍵掃描,可以獲取按鍵下邊沿、上邊沿、長按、短點擊、長按后置高、長按時間等多個動作,當按鍵產生動作后,執行相應操作(如改變采樣率、切換示波器/頻譜等)。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM14){Key_Handle(); // 在Key_Handle()中進行按鍵動作的操作} }對于EC11旋轉編碼器的處理,按鍵部分參考前述即可,左右旋轉(PB4、PA15)通過一側IO作為輸入時鐘捕獲,判斷另一側IO的電位即可,在此設置TIM2_CH1(對應PA15):
| Prescaler | 63 |
| Counter Period | 999 |
| Input Capture Channel 1參數如下: | |
| Parameters | Value |
| – | – |
| Polarity Selection | Falling Edge |
| IC Selection | Direct |
| Prescaler Dividion Ratio | No division |
| Input Filter | 4 |
OLED顯示
關于OLED顯示相關代碼參見display.c/.h、wave.c/.h及OLED庫
經典128*128 4-wire SPI OLED。SPI2參數設置如下:
將oled.c中的部分代碼修改:
修改為硬件SPI寫入,刪去清屏函數后的刷新可使OLED屏幕顯示不閃爍。
wave.c主要負責獲取的數據的處理,做線性映射以顯示在屏幕上,還做調整Y軸顯示范圍等功能。
display.c主要負責顯示波形,顯示文字、顯示其他信息等。
系統頂層
系統相關代碼參見user.c/.h
主要負責監視OLED的狀態與按鍵的動作:
typedef enum {Oscilloscope,Distortion }System_State; /* ------------------- */ System_State System = Oscilloscope; uint8_t Page_Init = 1; uint8_t zoom_in = 0x00; // left for zoom in, right for zooming out uint8_t zoom_out = 0x00; void System_Change_State(System_State State) {System = State;Page_Init = 1; } void OLED_Handle(void) {switch(System){case Oscilloscope:{if(Page_Init){Page_Init = 0;zoom_in = 0x00;zoom_out = 0x00;Oscilloscope_Init();}memset(FFT_Value, 0x0000, sizeof(FFT_Value));memset(ADC_Value, 0x0000, sizeof(ADC_Value));Source_Init();Sample_Init();Wave_View(ADC_Value, Get_SampleRate(), graph);break;}case Distortion:{if(Page_Init){Page_Init = 0;zoom_in = 0x00;zoom_out = 0x00;Distortion_Init();}Sample_Init();memcpy(FFT_Value, ADC_Value, sizeof(FFT_Value));if(ZeroFFT((int16_t*)FFT_Value, FFT_POINT)== 0){Spectrum_View((uint16_t*)FFT_Value, graph, Get_SampleRate());}break;}default:break;} }按鍵動作大致設置如下:
void Key_Handle(void) {static Key_Type Key[3] = {0};Get_Key(Key);switch(System){case Oscilloscope:{if(Get_Rise(Key1) || Get_Long_Tri(Key1)){if(is_setting_source_freq())Inc_SourceFreq();elseInc_SourceAmp();}if(Get_Rise(Key2) || Get_Long_Tri(Key2)){if(is_setting_source_freq())Dec_SourceFreq();elseDec_SourceAmp();}if(Get_Long_Press(KeyP)) // 長按旋鈕{toggle_display();System_Change_State(Distortion);}if(Get_Cont_Click(KeyP) == 2){toggle_scale();}if(Get_Rise(KeyP)) // 短按旋鈕{toggle_source_setting();}if(zoom_in == 0x01){if(is_auto_scale()){Dec_SampleRate();}else{Inc_YScale();}zoom_in = 0x00;}else if(zoom_out == 0x01){if(is_auto_scale()){Inc_SampleRate();}else{Dec_YScale();}zoom_out = 0x00;}break;}case Distortion:{if(Get_Long_Press(KeyP)) // 長按旋鈕{toggle_display();System_Change_State(Oscilloscope);}if(Get_Rise(KeyP)) // 短按旋鈕{toggle_spectrum_yaxis();}if(zoom_in == 0x01){Inc_SampleRate();zoom_in = 0x00;}else if(zoom_out == 0x01){Dec_SampleRate();zoom_out = 0x00;}break;}default:break;} }在示波器狀態下,按下板卡下方兩個按鈕,若目前是調整源頻率/幅度,增大/減小頻率/幅度;長按旋鈕,切換至頻譜/失真度曲線顯示。
功能展示
OLED顯示采樣波形
示波器頁面,中間橫線顯示直流電平所在處,左上角顯示采樣時間,中間顯示目前所調整的為源頻率/幅度,右上角標識當前頁面。下方左側顯示信號峰值、直流偏置電壓及目前Y軸顯示范圍;右側顯示源頻率、源幅度及Y軸顯示范圍自動/手動調整。
長按旋鈕,切換至頻譜/失真度頁面。
OLED顯示頻譜/失真度曲線
左上方顯示橫軸每格代表頻率,隨采樣率而變化,右上角標識當前頁面。下方左側標識當前為線性/對數坐標顯示,及THD;右側為通過FFT計算的中心頻率。
短按旋鈕即可切換線性/對數坐標。
👉 項目演示視頻參見:基于STM32G031的失真度測試儀
項目總結
由于此前作者純Keil與庫函數開發,此次項目接觸到了CubeMX與HAL庫工具鏈,一鍵生成MDK工程雀食方便,工程項目的排布省時省力。HAL庫在某些包裝上也有其獨特優勢,今后用在F407的開發試試。
👉工程文件及代碼:參見【2022寒假在家練】基于STM32G031的失真度測試儀
👉 CSDN:工程源代碼下載
👉 Github-KafCoppelia/DistortionMeter
總結
以上是生活随笔為你收集整理的基于STM32G031的失真度测试仪(CubeMX+ADC+DMA+OLED+EC11)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用 CMake 构建 LLVM
- 下一篇: 零基础Ar学习之Unity3D运行Eas