今天隨機(jī)分享4篇基于第一個平臺的項(xiàng)目:
“寒假在家一起練”(1) - 兩個月嵌入式編程DIY示波器和信號發(fā)生器,玩起來就免購板費(fèi)
本文為中國科技大學(xué)王赫男同學(xué)完成并分享的項(xiàng)目內(nèi)容。。
1 項(xiàng)目需求
完成對板上音頻信號的采集和波形顯示,可以通過手機(jī)播放音樂或App產(chǎn)生音頻信號的方式提供聲音信號源,通過板上電路的放大、MCU中ADC的采集以后將波形顯示在OLED屏幕上,可以通過板上按鍵的操作在兩個方向(橫軸 - 時(shí)間;縱軸 - 幅度)來擴(kuò)展、壓縮波形的顯示,按鍵的功能可自行定義;
實(shí)現(xiàn)信號發(fā)生器的功能,能夠產(chǎn)生2KHz以內(nèi)的正弦波、三角波、方波三種常用波形,通過按鍵的操作能夠?qū)崿F(xiàn)頻率可調(diào)、幅度可調(diào),通過調(diào)整板上的R、C的值,可以最高生成200KHz的模擬信號;
能夠通過Ain管腳測量外部模擬信號(0-3.3V,DC-200KHz),并能夠?qū)ν獠康闹芷谛圆ㄐ螠y量其周期和峰-峰值;
能夠?qū)Σ杉降男盘栠M(jìn)行FFT變換,并在屏幕上顯示其基頻及低次諧波(比如2、3、4、5次)的分量。
2 完成的功能及達(dá)到的性能
2.1 波形顯示
顯示波形時(shí),按下L提高采樣率,按下R降低采樣率,采樣率取值范圍為1kHz、2.5kHz、5kHz、10kHz、25kHz、50kHz、100kHz、250kHz、500kHz、1MHz,通過改變采樣率來實(shí)現(xiàn)橫軸的縮放。
Y軸(幅度范圍)默認(rèn)為自動調(diào)整,即程序自動根據(jù)采樣序列調(diào)整Y軸中心電壓值和縮放范圍,使波形完整顯示在屏幕上。通過菜單可以改為手動模式,即手動調(diào)整Y軸中心電壓值和Y軸縮放范圍。
左下角顯示波形參數(shù),可以顯示時(shí)間軸分度值、信號峰峰值、直流分量和頻率。
正下方顯示當(dāng)前狀態(tài),包含輸入通道、觸發(fā)狀態(tài)和前述的Y軸縮放方式(A:自動縮放,MO (Manual Offset):U/D按鍵調(diào)整Y軸中心電壓值,MS (Manual Scale):U/D按鍵調(diào)整Y軸縮放范圍。
按下OK鍵可以暫停波形刷新,再按可以繼續(xù)刷新。
2.2 觸發(fā)顯示和觸發(fā)菜單
程序默認(rèn)為上升沿觸發(fā),觸發(fā)電平為1.68V。顯示波形且觸發(fā)開啟時(shí),屏幕正下方顯示當(dāng)前觸發(fā)邊沿(上升沿、下降沿)和觸發(fā)狀態(tài)(箭頭點(diǎn)亮為觸發(fā)成功、背景點(diǎn)亮為觸發(fā)失敗)。
長按R鍵打開觸發(fā)菜單,在觸發(fā)菜單中可以開啟/關(guān)閉觸發(fā),選擇觸發(fā)邊沿,選擇自動觸發(fā)還是單次觸發(fā)。
2.3 示波器菜單
長按OK鍵打開示波器菜單,示波器菜單共有4項(xiàng),分別是:波形/頻譜顯示切換、Y軸縮放方式、波形參數(shù)切換、通道切換(麥克風(fēng)與板上信號輸入)。LRUD四個按鍵用來對上述四項(xiàng)功能進(jìn)行切換。
2.4 頻譜顯示
通過菜單切換至頻譜顯示時(shí),屏幕顯示信號的頻譜,顯示頻率范圍為直流至采樣頻率的一半。同樣按下L提高采樣率,按下R降低采樣率。左下角顯示頻率軸分度值。
2.5 信號輸出
長按L鍵打開輸出菜單,在輸出菜單中,可以開啟/關(guān)閉信號輸出,增加/降低輸出信號的頻率(步長100Hz,上限2kHz)、峰峰值(步長0.1V,上限3.3V)和調(diào)整輸出波形(正弦波、三角波、方波)。
3 實(shí)現(xiàn)思路
ADC對模擬輸入進(jìn)行采樣,采樣由定時(shí)器觸發(fā),采樣結(jié)果由DMA搬運(yùn);
將采樣得到的ADC量化值映射到屏幕坐標(biāo)點(diǎn)上,實(shí)現(xiàn)波形顯示;
按下按鍵調(diào)整采樣頻率,實(shí)現(xiàn)波形在時(shí)間軸上的擴(kuò)展與壓縮;
對采樣序列進(jìn)行FFT變換,繪制頻譜;
信號參數(shù)的顯示,如峰峰值、直流分量、信號頻率等;
輸出PWM波并通過RC低通濾波實(shí)現(xiàn)方波、正弦波、三角波的生成,通過按鍵改變PWM波的頻率與占空比,從而改變輸出信號的頻率和幅度。
4 實(shí)現(xiàn)過程
4.1 程序流程圖
注:每個框圖右下角名稱為執(zhí)行該功能的主要文件
4.2 ADC對數(shù)據(jù)進(jìn)行采樣
為了方便進(jìn)行FFT計(jì)算,ADC共采集256個采樣點(diǎn)。每次ADC轉(zhuǎn)換由定時(shí)器1觸發(fā),觸發(fā)頻率最高為1MHz,即ADC采樣率最高為1Msps。ADC的轉(zhuǎn)換結(jié)果直接由DMA搬運(yùn)至內(nèi)存。
ADC轉(zhuǎn)換開始函數(shù)(定義位置:sample.c,調(diào)用位置:main.c):
/** * @brief Start a new sample sequence. * @param[in] ADCValue Array to store incoming sample values. * @retval None */ void start_sample(uint16_t *ADCValue) { HAL_Delay(1); HAL_ADCEx_Calibration_Start(&hadc1); HAL_ADC_Start_DMA(&hadc1, (uint32_t *)ADCValue, SAMPLE_POINTS); }
256次轉(zhuǎn)換結(jié)束后進(jìn)入中斷,置位結(jié)束標(biāo)志位,進(jìn)入后續(xù)的數(shù)據(jù)處理程序。
ADC轉(zhuǎn)換結(jié)束中斷回調(diào)函數(shù)(定義位置:adc.c):
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if(hadc == &hadc1) { finish_sample(); } }
4.3 采樣結(jié)果的處理
得到256個采樣的ADC量化值后,根據(jù)觸發(fā)電平選擇波形起始點(diǎn),返回起始點(diǎn)在數(shù)組中的下標(biāo),顯示從起始點(diǎn)開始的100個點(diǎn)。
波形觸發(fā)部分代碼(定義位置:wave.c,調(diào)用位置:app.c,其中total_points=256, GRAPH_WIDTH=101):
/** * @brief Wave trigger. * @param[in] ADCValue Array of sampled ADC values. * @param[in] total_points Total sampled points. * @retval Index of the trigger start point(》1)。 0 means trigger off or failed. */ uint16_t trigger(uint16_t *ADCValue, uint16_t total_points) { uint16_t i; uint16_t trigger_value = VOL2ADC(1.68); if (!is_trigger_on()) return 0; for (i = 1; i 《 total_points - GRAPH_WIDTH + 2; i++) { if (get_trigger_edge()) // falling edge { if (ADCValue[i-1] 》 trigger_value && ADCValue[i] 《= trigger_value) { trigger_success(); if (is_trigger_single()) pause(); return i; } } else { if (ADCValue[i-1] 《= trigger_value && ADCValue[i] 》 trigger_value) { trigger_success(); if (is_trigger_single()) pause(); return i; } } } trigger_fail(); return 0; }
取起始點(diǎn)后100個采樣值使其顯示在OLED屏幕上(一次性刷新)。為此需要將ADC量化值與OLED屏幕上的坐標(biāo)進(jìn)行線性映射。在自動模式(自動縮放y軸)中,程序自動找出量化值中的最大最小值,并使最大最小值也能不超出繪制范圍以外,這樣屏幕就可以顯示完整的波形。
自動縮放y軸代碼(定義位置:wave.c,調(diào)用位置:app.c):
/** * @brief Automatically find the central/max/min voltage on y-axis. * @param[in] ADCValue Array of sampled ADC values. * @note The function calculates the min/max voltage of the sampled signal, * then find a proper scale voltage and a central voltage on y-axis. * @retval None */ void auto_scale(uint16_t *ADCValue) { uint16_t a_max_value, a_min_value, a_pp_value; float exact_voltage, floor_voltage, ceil_voltage; get_max_min_pp_value(ADCValue, &a_max_value, &a_min_value, &a_pp_value); voltage_range_auto_select(ADC2VOL(a_pp_value/2)); exact_voltage = ADC2VOL(a_max_value + a_min_value) / 2; floor_voltage = (uint8_t)(ADC2VOL((a_max_value + a_min_value)*5)) / 10.0; //keep one decimal ceil_voltage = floor_voltage + 0.1; // round center_voltage volt_on_y_axis.center_voltage = ceil_voltage - exact_voltage 《 exact_voltage - floor_voltage ? ceil_voltage : floor_voltage; volt_on_y_axis.max_voltage = volt_on_y_axis.center_voltage + v_scale_list[v_scale_index]; volt_on_y_axis.min_voltage = volt_on_y_axis.center_voltage - v_scale_list[v_scale_index]; }
坐標(biāo)映射代碼(定義位置:wave.c,調(diào)用位置:app.c):
/** * @brief Generate y-coordinates of the wave. * @param[in] ADCValue Array of sampled ADC values. * @param[out] y Y-coordinate array of the wave. * @note The function map ADCValues to OLED y coordinates. * @retval None */ void generate_wave(uint16_t *ADCValue, uint8_t *y) { // Quantize y-axis min/max/central voltages to ADC values. int16_t a_max_value = VOL2ADC(volt_on_y_axis.max_voltage); int16_t a_min_value = VOL2ADC(volt_on_y_axis.min_voltage); uint8_t i; // Linearly map every ADC value to its coordinate. for (i = 0; i 《 GRAPH_WIDTH - 1; i++) { if (ADCValue[i] 《= a_max_value && ADCValue[i] 》= a_min_value) y[i] = (GRAPH_HEIGHT - 1) * (a_max_value - ADCValue[i]) / (a_max_value - a_min_value) + GRAPH_START_Y; else if (ADCValue[i] 》 a_max_value) y[i] = GRAPH_START_Y; else if (ADCValue[i] 《 a_min_value) y[i] = GRAPH_HEIGHT + GRAPH_START_Y - 1; } }
波形顯示代碼(定義位置:display.c,調(diào)用位置:app.c):
/** * @brief Display wave on OLED. * @param[in] y Y-coordinate array of the wave. * @retval None */ void display_wave(const uint8_t *y) { uint8_t x; for (x = GRAPH_START_X; x 《 GRAPH_WIDTH - 1; x++) OLED_DrawLine(x, y[x-GRAPH_START_X], x + 1, y[x-GRAPH_START_X+1], 1); OLED_DrawPoint(x, y[x-GRAPH_START_X], 1); }
在手動模式中,可以手動調(diào)節(jié)y軸的縮放范圍和y軸中心電壓值,但此時(shí)波形不一定會完整顯示。得到采樣點(diǎn)坐標(biāo)后,使用OLED的繪制直線函數(shù),連接屏幕上各個離散的點(diǎn),就可以得到信號的波形。
當(dāng)需要顯示頻譜時(shí),就需要對所有的ADC的量化值進(jìn)行256點(diǎn)FFT變換,由于FFT變換結(jié)果關(guān)于中心點(diǎn)對稱,且屏幕x方向分辨率為128點(diǎn),所以保留FFT需要為0~127的結(jié)果,進(jìn)行線性映射后顯示在屏幕上。
FFT的代碼定義在fftutil.c中,對變換結(jié)果的處理及顯示分別定義在spectrum.c和display.c中。
4.4 信號發(fā)生器
板上有一個1Kohm的電阻和10nF的電容構(gòu)成的低通濾波器,截止頻率為1.6KHz。若在該輸出端輸出頻率足夠的PWM信號,則輸出電壓大小就和PWM的占空比成正比。通過改變PWM的占空比就可以調(diào)節(jié)輸出電壓波形。通過實(shí)驗(yàn)可知,當(dāng)信號的每一個周期由500個PWM脈沖組成時(shí),信號的紋波較小。
以正弦信號為例,在程序外,在電腦中生成一個正弦信號,并在一個周期中進(jìn)行500次采樣,根據(jù)電壓和PWM占空比的正比關(guān)系可以計(jì)算出500個PWM脈沖的占空比。將其定義為長度為500的數(shù)組寫入程序。程序中使能PWM的DMA通道,這樣就可以在每個PWM脈沖結(jié)束后自動將數(shù)組中的元素載入定時(shí)器輸出比較寄存器,從而改變占空比。低通濾波器再將STM32產(chǎn)生的PWM脈沖轉(zhuǎn)變?yōu)槟M信號,即可重新生成正弦波。方波和三角波同理。
開啟PWM和DMA代碼(定義位置:source.c,調(diào)用位置:app.c,其中SIGNAL_LENGTH=500):
/** * @brief Start signal output at Aux. * @retval None */ void start_output(void) { HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_2, (uint32_t *)output_wave_value, SIGNAL_LENGTH); }
信號的幅度調(diào)節(jié)可以直接對上述數(shù)組每個元素乘一個常數(shù)來實(shí)現(xiàn);頻率調(diào)節(jié)首先要調(diào)節(jié)定時(shí)器的自動重載值(ARR),改變PWM的頻率。為保證幅度不變,數(shù)組中每個元素也要同比例縮放。
5 遇到的主要難題
5.1 中斷與DMA
項(xiàng)目共有兩處使用DMA,分別用于儲存ADC采樣結(jié)果和調(diào)整輸出PWM定時(shí)器的自動重載值(ARR)。如果用中斷處理數(shù)據(jù)而非DMA,則會產(chǎn)生以下問題:
若在ADC轉(zhuǎn)換完成中斷中讀取轉(zhuǎn)換結(jié)果,則在一次采樣序列(256點(diǎn))中,中斷頻率過于頻繁,且由于中斷耗時(shí),無法得到很高的采樣率,最高只能達(dá)到幾十kHz。若使用DMA,則只需在整個采樣序列結(jié)束后進(jìn)入中斷,不會對采樣造成影響。
若使用PWM中斷更新自動重載值,中斷耗時(shí)會使PWM頻率產(chǎn)生偏差,且會對OLED屏幕的SPI時(shí)序造成影響,導(dǎo)致屏幕無法正常顯示。若使用DMA更新自動重載值,則不需要PWM中斷,更新耗時(shí)相比于中斷有很大改善。
綜上所述,在頻率較高或需要頻繁更新數(shù)據(jù)的情況,中斷會帶來各種各樣的問題,而DMA則可以高效完成任務(wù)。
5.2 RAM和Flash大?。‵FT優(yōu)化)
項(xiàng)目使用的FFT算法根據(jù)Adafruit ZeroFFT修改而來。該算法最高可支持4096點(diǎn)FFT,其旋轉(zhuǎn)因子表、窗函數(shù)表和信號序列數(shù)組占用空間極大。而本項(xiàng)目使用的STM32G031G8只有64K的Flash和8K的RAM,資源極為有限,無法直接運(yùn)行ZeroFFT。
為此需要對ZeroFFT的代碼進(jìn)行優(yōu)化。該項(xiàng)目只需256點(diǎn)FFT,刪去256點(diǎn)之外的部分,縮短查找表,能極大減小RAM和Flash占用。
具體的優(yōu)化步驟:
將Adafruit_ZeroFFT.h中的宏定義ZERO_FFT_MAX改為512。(對應(yīng)256點(diǎn)FFT)
刪去fftutil.c中ZeroFFT函數(shù)所有其他點(diǎn)數(shù)的FFT代碼,只保留256點(diǎn)FFT的代碼。同樣刪去窗函數(shù)中256點(diǎn)以外的部分和窗函數(shù)查找表。
此時(shí)fftutil.c中只調(diào)用了arm_common_tables.c中armBitRevTable和twiddleCoefQ15兩個查找表,刪去其他所有數(shù)組。
在fftutil.c中所有調(diào)用armBitRevTable和twiddleCoefQ15查找表的代碼下面添加printf,用PC運(yùn)行FFT程序,打印調(diào)用的下標(biāo)。
以twiddleCoefQ15數(shù)組為例,原長度為6144;對于256點(diǎn)FFT,只有其中384個值被調(diào)用。PC中編寫一個臨時(shí)程序,根據(jù)調(diào)用的下標(biāo),用printf打印一個新的長度為384的查找表替換掉原來的。另一個查找表同理。
fftutil.c中部分變量代表查找表的步進(jìn)值,查找表改變后這些步進(jìn)值也要改變。
此時(shí)FFT的代碼應(yīng)該就可以在STM32G0上運(yùn)行了~
此外,由于Flash和RAM的資源有限,在FFT之外的其他很多地方也需要對空間進(jìn)行優(yōu)化,比如刪去oled不需要的字庫等。
5.3 PWM輸出頻率
由于電容的充放電,由PWM經(jīng)過低通濾波輸出的信號會有鋸齒,信號幅度較低時(shí)鋸齒更為明顯,并會造成波形顯示的不穩(wěn)定。開始時(shí)輸出信號一個周期內(nèi)有50個PWM脈沖,即PWM的頻率是信號頻率的50倍,當(dāng)信號幅度較低時(shí)鋸齒極為明顯,對輸出波形造成極大干擾。將一個周期內(nèi)PWM脈沖數(shù)提升至500,鋸齒密度變大,同時(shí)幅度減小,對輸出信號的干擾也減小。但同時(shí)儲存輸出信號幅度信息的查找表也變大10倍,消耗了更多的空間。
6 未來的計(jì)劃建議
該項(xiàng)目已經(jīng)成功實(shí)現(xiàn)了簡易示波器和信號發(fā)生器的功能,并達(dá)到了預(yù)期指標(biāo)。然而通過更換硬件,還有許多可以提升與擴(kuò)展的地方:
板上的OLED屏幕分辨率較低,無法顯示信號細(xì)節(jié)與更多信息??梢允褂梅直媛矢叩钠聊唬?qū)⒉ㄐ涡畔⒅苯影l(fā)送給上位機(jī),由上位機(jī)進(jìn)行顯示。
主控芯片STM32G031的資源有限。可以更換更好的主控芯片,來提高采樣率,采樣點(diǎn)數(shù)等從而實(shí)現(xiàn)更高的性能。
可以對輸入信號進(jìn)行衰減,從而增大輸入信號的電壓范圍。
增加模擬輸入的通道,并添加波形的數(shù)學(xué)運(yùn)算功能,如波形之間的加減。
改變輸出端的RC值,擴(kuò)展輸出信號頻率范圍。
不更換硬件可以提升與擴(kuò)展的地方(懶得做的部分):
自動/手動調(diào)整觸發(fā)電平。
改變輸入信號耦合方式(直流/交流耦合)。
對輸入信號進(jìn)行數(shù)字濾波。
信號源實(shí)現(xiàn)更高的頻率分辨率。
原文標(biāo)題:如何在STM32G031上實(shí)現(xiàn)示波器和頻譜分析功能?
文章出處:【微信公眾號:FPGA入門到精通】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
責(zé)任編輯:haq
-
示波器
+關(guān)注
關(guān)注
113文章
6276瀏覽量
185526 -
STM32
+關(guān)注
關(guān)注
2270文章
10915瀏覽量
356787 -
信號發(fā)生器
+關(guān)注
關(guān)注
28文章
1479瀏覽量
108878
原文標(biāo)題:如何在STM32G031上實(shí)現(xiàn)示波器和頻譜分析功能?
文章出處:【微信號:xiaojiaoyafpga,微信公眾號:電子森林】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論