引言
寒假練有一款白色的、非常美觀的、雙通道輸入的基于STM32G031的板卡,它可以實(shí)現(xiàn)哪些功能呢?示波器、DDS信號(hào)發(fā)生器、頻譜分析儀、失真度測(cè)量?jī)x等等。 今天我們來看一位來自南京大學(xué)的【電子卷卷怪】同學(xué)所做的雙通道簡(jiǎn)易示波器項(xiàng)目,這位同學(xué)還幫助多個(gè)參加寒假練的同學(xué)親自解決了他們的問題。項(xiàng)目成果概述
本項(xiàng)目使用硬禾課堂STM32G031開發(fā)板卡以及STM32CubeIDE開發(fā)工具,實(shí)現(xiàn)了一個(gè)簡(jiǎn)易的示波器。示波器的各項(xiàng)參數(shù)或功能概述如下:
1. 外觀(1)有主界面、副界面兩個(gè)界面,并可以相互切換;
(2)主界面包含波形模式和FFT模式,分別顯示被測(cè)信號(hào)的波形和頻譜;
(3)波形模式包含:垂直尺度調(diào)整、水平時(shí)基調(diào)整、屏幕中心電平調(diào)整、模擬觸發(fā)電平調(diào)整、負(fù)時(shí)間調(diào)整、平均值顯示、頻率測(cè)量顯示、峰峰值顯示;FFT模式包含:垂直尺度顯示、采樣率顯示、屏幕中心電平顯示、模擬觸發(fā)電平顯示、頻譜最大分量(歸一化值)顯示、頻標(biāo)調(diào)整、頻標(biāo)對(duì)應(yīng)分量顯示。
(4)副界面包含5個(gè)其他功能:通道選擇、波形/FFT模式切換、開啟AUTO、模擬觸發(fā)電平極性、開啟單次(Single)模式。
2. 操作
(1)位于主界面的任意模式時(shí),單擊左右鍵可以使光標(biāo)在該模式下可調(diào)整的功能間移動(dòng),轉(zhuǎn)動(dòng)旋鈕調(diào)整被光標(biāo)選中的參數(shù);
(2)位于副界面時(shí),單擊左右鍵可以使光標(biāo)在5個(gè)其他功能間移動(dòng),轉(zhuǎn)動(dòng)旋鈕可以調(diào)整被選中的功能;
(3)按住旋鈕的情況下:?jiǎn)螕糇箧I進(jìn)入主界面、單擊右鍵進(jìn)入副界面。
(4)開啟AUTO后,自適應(yīng)調(diào)整只會(huì)在切回主界面后被執(zhí)行一次;對(duì)新波形的自適應(yīng)調(diào)整需要切到副界面——開啟AUTO——切回主界面。
(5)開啟Single后,無觸發(fā)時(shí),正常顯示波形;觸發(fā)一次后,波形與頻譜均固定,并不會(huì)更新,但可以調(diào)整負(fù)時(shí)間和頻標(biāo);在觸發(fā)后,調(diào)整垂直尺度、水平時(shí)基、屏幕中心電平、模擬觸發(fā)電平、采樣率中的任意一者,都會(huì)導(dǎo)致下一次觸發(fā)的捕捉。 ?
項(xiàng)目需求分析
總的來說,本項(xiàng)目可以分為兩個(gè)大的模塊:GUI模塊、采樣處理模塊。其中,相對(duì)于程序的主循環(huán)而言,采樣處理模塊是高速的、“同步”的,GUI模塊是慢速的、“異步”的。兩個(gè)模塊間既需要并行不悖,又需要互相交換數(shù)據(jù)。 對(duì)于采樣處理模塊,主要考慮以下4個(gè)需求問題:1. ADC可控采樣率與切換通道的實(shí)現(xiàn);2. 觸發(fā)電平的實(shí)現(xiàn),以及負(fù)時(shí)間顯示的實(shí)現(xiàn);3. 如何對(duì)頻率進(jìn)行較高精度的測(cè)定;4. 如何計(jì)算信號(hào)頻譜; 對(duì)于GUI模塊,主要考慮以下3個(gè)需求問題:1. 如何以盡可能低的誤判率獲取按鍵與旋鈕的信息;2. 中斷服務(wù)函數(shù)所應(yīng)干涉的范圍;3. 如何以盡可能簡(jiǎn)潔的方式實(shí)現(xiàn)按鍵對(duì)GUI的改變 對(duì)于兩個(gè)模塊而言,最核心的問題是:如何在兩者之間進(jìn)行高效的數(shù)據(jù)傳輸?shù)耐瑫r(shí),避免數(shù)據(jù)的誤判或漏判。核心技術(shù)路線
針對(duì)“二”中提出的需求,以下同樣分兩個(gè)模塊,對(duì)項(xiàng)目的技術(shù)路線進(jìn)行完備的論述。鑒于HAL庫過于龐大,且本人對(duì)項(xiàng)目的理解更偏重于硬件底層,除了HAL_Init,SystemClock_Config,以及與NVIC有關(guān)的3個(gè)最底層的函數(shù)(Priority, Enable, ClearPending)外,其他所有的外設(shè)配置代碼,均為本人閱讀器件手冊(cè)后編寫的寄存器代碼。1. ADC可控采樣率與通道切換
在ADC連續(xù)模式下,雖然可以通過調(diào)整采樣時(shí)間來調(diào)整采樣率,但這樣做顯然并不好。一方面,這樣得到的轉(zhuǎn)換周期(Tsamp + 12.5ADC_Cycle)的倒數(shù),即頻率,往往是不規(guī)律的非整數(shù),這樣做不利于功能調(diào)整的層次化與統(tǒng)一化;另一方面,即使采用16MHz主頻,在12位分辨率下,ADC最小轉(zhuǎn)化頻率也有16MHz / (160.5 + 12.5) ≈92.5kHz,有效測(cè)量范圍太小。 定時(shí)器觸發(fā)的方式是最好的選擇。一方面,只需控制轉(zhuǎn)換時(shí)間不大于采樣率的倒數(shù),就能獲得完全可控的轉(zhuǎn)換率;另一方面,這樣有利于定時(shí)器觸發(fā)DMA傳輸?shù)囊搿S捎谠?2MHz主頻下,即使是最簡(jiǎn)單的中斷服務(wù)函數(shù),頻率也只能到150kHz左右,因此,DMA傳輸既可以提供較高的采樣率,又可以使“采樣——處理”分離的結(jié)構(gòu)更加清晰。配置的方法: 對(duì)ADC端:
模擬看門狗的配置將在后面說明。這里最關(guān)鍵的,一是必須配置為非連續(xù)模式、外部上升沿觸發(fā),選擇TIM2的TRGO為觸發(fā)源,并且不能選擇ADC為DMA觸發(fā)源,否則ADC的overwritten特性會(huì)迫使軟件屢屢清除標(biāo)志位,以保證DMA Request的持續(xù)產(chǎn)生;二是在外部觸發(fā)時(shí),必須先start。 ?對(duì)DMA端:void ADC_init(void)
{
uint32_t temp;
RCC->IOPENR |= 0X1UL;//打開PortA時(shí)鐘
temp=RCC->IOPENR;//時(shí)鐘使能需等2個(gè)周期
UNUSED(temp);//避免Warning
//由于GPIOA->MODER對(duì)應(yīng)位默認(rèn)為0X3,即模擬輸入
//因此不需要再額外配置PortA
RCC->APBENR2 |= (0X1UL<<20UL);//打開ADC1時(shí)鐘
temp=RCC->APBENR2;
UNUSED(temp);
ADC1->CR |= (0X1UL<<28UL);//使能內(nèi)部參考電壓
//自己寫的延時(shí),用TIM17的OPM模式
TIM17_Delay(1000-1,32-1);//等待參考電壓有效
ADC1->CR |= (0X1UL<<31UL);
do
{
temp=ADC1->CR;//開始校正指令
}while(temp & (0X1UL<<31));//等待校正結(jié)束
ADC1->CFGR1 |= (0X1UL<<16 | 0X1UL<<12 | 0X2UL<<10 | 0X2UL<<6 | 0X0UL);
//(discontinuous,overwritten,ext rising edge,TRG2,DMA disabled);
ADC1->TR1 &= ~(0X0FFF0000);
ADC1->TR1 |= (0X0FFF0800);
//模擬看門狗的高低閾值
ADC1->CFGR1 |= (0X1<<26 | 0X1<<22 | 0X1UL<<23);
//AWD1 configuration
ADC1->CFGR2 |= (0X3UL<<30); //PCLK as ADC_CLK
ADC1->CHSELR |= (0X1UL << 1 | 0X0UL<<7);//選擇通道一
do
{
temp=ADC1->ISR;
}while(!(temp & (0X1UL<<13)));//等待通道配置有效
ADC1->CR |= 0X1UL;//enabling ADC1
do
{
temp = ADC1->ISR;
}while(!(temp & 0X1UL));//ADC Ready
ADC1->CR |= 0X1UL<<2;//ADC Start
return;
}
傳輸數(shù)據(jù)使用的是通道一。相比于F407等系列,G031引入了DMAMUX的概念,使得幾乎所有的外設(shè)和一些事件都可以在任意一個(gè)DMA通道上產(chǎn)生請(qǐng)求。由于DMAMUX的0~4對(duì)應(yīng)DMA的1~5,查閱用戶指南后,得知設(shè)置DMAMUX的CCR的低7位為31(0X1F)表示TIM2的Update。 對(duì)TIM端:void ADC_DMA_init(void)
{
uint32_t temp;
RCC->AHBENR |= 0X1UL;
temp=RCC->AHBENR;//時(shí)鐘使能需2個(gè)周期
UNUSED(temp);//避免Warning
DMA1_Channel1->CPAR = (uint32_t)(ADC1_BASE+0X40);
DMA1_Channel1->CMAR = (uint32_t)(&dat_buf);
DMA1_Channel1->CNDTR = ADC_MAX * 2;
DMA1_Channel1->CCR |= (0X2UL<<12 | 0X1UL<<10 | 0X2UL<<8 | 0X1UL<<7 |
0X0UL<<3 | 0X1UL<<1 | 0X1 << 5);
//v-high priority, m-size=16,p-size=16,m-increase,
//error and complete interrupt, circular mode;
DMAMUX1_Channel0->CCR &= ~(0X7FUL);
DMAMUX1_Channel0->CCR |= (0X1FUL);//tim2 as request source
__NVIC_SetPriority(DMA1_Channel1_IRQn,0);
__NVIC_EnableIRQ(DMA1_Channel1_IRQn);
DMA1_Channel1->CCR |= 0X1UL;//enable DMA channel
return;
}
通過CR2的主模式位MMS[6:4]配置TIM2的Update為TRGO,否則無法正確觸發(fā)ADC;使能更新事件的DMA請(qǐng)求。 在上述框架下,DMA只要開啟單次模式,等待全傳輸中斷函數(shù)置標(biāo)志位就可以了。需要注意的是,在清除中斷標(biāo)志的時(shí)候,需要同時(shí)清除NVIC端和外設(shè)端的標(biāo)志位,否則會(huì)陷入無限的中斷循環(huán)。 若開啟了上述外設(shè)配置,則上述架構(gòu)在DMA One shot模式下就能完成采樣率可調(diào)的循環(huán)數(shù)據(jù)傳輸。而我們最終開啟的是DMA Circular模式,這將在后面說明。void TIM2_Init(unsigned int priority)
{
uint32_t temp;
RCC->APBENR1 |= 0X1UL;//使能TIM2時(shí)鐘
temp=RCC->APBENR1;
UNUSED(temp);
//TIM2->DIER |= 0X1UL;//允許更新中斷
TIM2->CR1 |= 0X1UL<<2UL;//手動(dòng)更新不觸發(fā)中斷
TIM2->CR2 |= 0X2<<4;//update as TRGO
TIM2->SMCR |= 0X1UL<<7;
TIM2->DIER |= 0X1UL<<8;
TIM2->ARR = 16-1;
TIM2->PSC = 0;
temp=TIM2->ARR;
TIM2->EGR |= 0X1UL;//手動(dòng)更新寄存器值
temp=TIM2->PSC;
UNUSED(temp);
}
2. 觸發(fā)電平的實(shí)現(xiàn),以及負(fù)時(shí)間的實(shí)現(xiàn)
觸發(fā)電平,即以被測(cè)信號(hào)越過某個(gè)閾值電壓為起算點(diǎn),采集后面的若干個(gè)數(shù)據(jù)。該方法可以使波形穩(wěn)定地顯示在屏幕上。 負(fù)時(shí)間,即可以顯示觸發(fā)電平前一定時(shí)間內(nèi)的波形。當(dāng)觸發(fā)電平用于異常信號(hào)的單次捕捉(Single模式)時(shí),負(fù)時(shí)間可以顯示異常信號(hào)前的波形。 有同學(xué)在無條件采樣后計(jì)算一組數(shù)據(jù)的均值(中值),并顯示從中值樣點(diǎn)開始的數(shù)據(jù),從而通過軟件實(shí)現(xiàn)觸發(fā)電平。這種方案在實(shí)現(xiàn)AUTO時(shí)不失為一個(gè)好的啟發(fā),但在此面臨兩個(gè)問題:第一,單純的中值判斷無法控制觸發(fā)的極性,即無法選擇上升沿還是下降沿觸發(fā)。若增加前后值判斷,則將增加軟件運(yùn)算量;第二,這種算法下不可能出現(xiàn)“無觸發(fā)”的、波形亂晃的現(xiàn)象,與真實(shí)的數(shù)字示波器存在差異。從本質(zhì)上講,這種方法沒有充分利用硬件底層。 G031的ADC自帶一個(gè)模擬看門狗,即Analog Window Watchdog的特性。即當(dāng)采樣值超出規(guī)定范圍(窗口)時(shí),輸出AWD_OUT將持續(xù)拉高,直至電壓落回窗口內(nèi),延遲為一個(gè)轉(zhuǎn)換周期。并且,這個(gè)信號(hào)是硬件連接(hardwired)至TIM1的外部觸發(fā)MUX的。它可以通過TIM1的AF1寄存器被選擇為TIM1的從模式外部觸發(fā)信號(hào)。 ?
配置TIM1從模式為Trigger Mode(上升沿觸發(fā)啟動(dòng))、選擇觸發(fā)源為外部觸發(fā)ETR,再連接AWD1至ETR,就可以在DMA One Shot模式下,實(shí)現(xiàn)基于硬件的、真正的觸發(fā)電平功能。通過ADC的TR1設(shè)置閾值,假設(shè)TIM1為上升沿啟動(dòng),則當(dāng)窗口為(x , 0x0FFF)時(shí),為下降沿觸發(fā);當(dāng)窗口為(0x0000 , x)時(shí),為上升沿觸發(fā)。
然而在這樣的結(jié)構(gòu)下,是無法實(shí)現(xiàn)負(fù)時(shí)間功能的。由于AWD_OUT的上升沿是不可預(yù)知的隨機(jī)事件,因此應(yīng)該對(duì)程序結(jié)構(gòu)進(jìn)行微調(diào):改用DMA Circular模式,AWD_OUT作為采樣停止——而不是開始——的信號(hào)。 假如我們希望采集觸發(fā)后的256個(gè)數(shù)據(jù)(為方便FFT運(yùn)算),又希望顯示負(fù)時(shí)間的128個(gè)數(shù)據(jù),則應(yīng)該配置TIM2為ADC觸發(fā)源,令TIM1的溢出周期為TIM2的256倍。在TIM1的中斷服務(wù)函數(shù)中關(guān)掉(Disable)TIM2,就能實(shí)現(xiàn)上述功能。與此同時(shí),DMA1_Channel1的CNDTR中將保存一個(gè)循環(huán)中剩余待傳輸?shù)臄?shù)據(jù)個(gè)數(shù),據(jù)此可以定位連同負(fù)時(shí)間在內(nèi)的整段有效數(shù)據(jù)在DMA目標(biāo)數(shù)組內(nèi)的起止位置。
?
若目標(biāo)數(shù)組大小為512,當(dāng)TIM2停止時(shí),CNDTR的值為CH1_CNDTR,則觸發(fā)點(diǎn)下標(biāo)應(yīng)為(512 - CH1_CNDTR - 256) % 512= (512 - CH1_CNDTR + 256) % 512= (768 - CH1_CNDTR) % 512 然而這樣的設(shè)計(jì)存在一個(gè)問題:模擬觸發(fā)事件具有隨機(jī)性,如果它在重新開啟TIM2后的幾個(gè)周期內(nèi)就發(fā)生,那么當(dāng)新一段數(shù)據(jù)被存儲(chǔ)完成后,負(fù)時(shí)間位置的數(shù)據(jù)還是上次采樣的數(shù)據(jù),這就會(huì)導(dǎo)致負(fù)時(shí)間顯示錯(cuò)誤。
為了避免上述情況,在新一輪開啟后,必須先等待一次全傳輸中斷再開啟TIM1。事實(shí)上,只要一次全傳輸中斷后,無論TIM1隔多久開啟,數(shù)組中的時(shí)間軸都是連續(xù)的。用dat_buf_ready的bit0表示全傳輸中斷、bit7表示TIM1中斷。
3. 信號(hào)頻率的測(cè)定if((!(cursor_buf & (0X1 << 7))) || ((cursor_buf & (0X1 << 7)) && (single_flag == 0)))
{
TIM1->ARR = TIM2->ARR;
TIM1->PSC = (TIM2->PSC + 1) * 256 - 1;
TIM1->EGR |= 0X1;
{
DMA1_Channel1->CNDTR = ADC_MAX * 2;
DMA1_Channel1->CCR |= 0X1UL;
TIM1->SR &= ~(0X1UL);
TIM2->CR1 |= 0X1UL;
}
while(!(dat_buf_ready & 0X01))
{
}
TIM1->DIER |= (0X1UL);
TIM1->SMCR |= (0X0UL<<16 | 0X6UL);
dat_buf_ready &= ~(0X1);
}
void TIM1_BRK_UP_TRG_COM_IRQHandler(void)
{
CH1_CNDTR = DMA1_Channel1->CNDTR;//賦值了不一定用,但這樣最準(zhǔn)確
if(TIM1->SR & 0X1UL)
{
{
TIM2->CR1 &= ~(0X1UL);
TIM1->CR1 &= ~(0X1UL);
//Stop tim2 and consequently stop DMA
TIM2->CNT = 0;//resetting TIM2
dat_buf_ready |= 0X1 << 7;//setting complement flag
}
TIM1->SR &= ~(0X1UL);
__NVIC_ClearPendingIRQ(TIM1_BRK_UP_TRG_COM_IRQn);
}
}
void DMA1_Channel1_IRQHandler(void)//中斷服務(wù)函數(shù)
{
DMA1->IFCR |=0X1UL;
dat_buf_ready |= 0X1;
__NVIC_ClearPendingIRQ(DMA1_Channel1_IRQn);
}
數(shù)字測(cè)定頻率的方法,一般是先整形再測(cè)量。即通過施密特觸發(fā)器(比如TLV3501)先把信號(hào)整形成脈沖,再對(duì)脈沖進(jìn)行測(cè)定。對(duì)脈沖的測(cè)定也有兩種思路:一是直接同步采樣后計(jì)算脈沖個(gè)數(shù),適用于較高頻率;二是計(jì)算脈沖高低電平的周期個(gè)數(shù),適用于較低頻率。兩種方法均受限于系統(tǒng)最高主頻。這也是本項(xiàng)目至今為止兩個(gè)尚為得出最優(yōu)解的難點(diǎn)之一。 本項(xiàng)目從脈沖整形到計(jì)數(shù)均采用硬件特性為主、軟件程序?yàn)檩o的思路。根據(jù)前面的討論可知,ADC的AWD在一定頻率以下等效于一個(gè)極其理想的脈沖整形器。相較于模擬施密特觸發(fā)器,其最大的特點(diǎn)在于脈沖整形的響應(yīng)特性與信號(hào)峰峰值的絕對(duì)值無關(guān),而僅受到信道噪聲和量化噪聲的干擾。因此,測(cè)量頻率最基本的方法,也是本項(xiàng)目采用的方法,就是對(duì)AWD的輸出信號(hào)AWD_OUT在一定時(shí)間內(nèi)進(jìn)行計(jì)數(shù)。此方法實(shí)現(xiàn)起來最為簡(jiǎn)單,但面臨兩個(gè)很大的問題:第一,相比于FPGA廣泛采用的雙閘門法,此方法會(huì)把閘門時(shí)間的前后沿漏掉,引入一定的誤差,但這并非主要矛盾。
第二,實(shí)測(cè)表明,在測(cè)定較低頻率的正弦波或三角波時(shí),頻率將出現(xiàn)較大誤差,只有對(duì)方波的測(cè)定最為準(zhǔn)確。這種誤差只有在200Hz以上才可以忽略不計(jì)。究其本質(zhì),是因?yàn)樾盘?hào)的噪聲抖動(dòng)所致。AWD_OUT的靈敏度帶來了一個(gè)致命的缺點(diǎn):沒有任何的滯回特性,這就導(dǎo)致在過觸發(fā)點(diǎn)附近的任何噪聲都可能被極大地放大,只有邊沿極抖的方波才能“幸免”。反觀模擬脈沖整形電路,由于人為設(shè)計(jì)滯回電路以及電路本身輸入輸出電容的存在,對(duì)輸入信號(hào)總有一定的消抖能力。當(dāng)然,用于傳輸測(cè)試信號(hào)的信道本身也存在問題。一方面,用于輸出測(cè)試信號(hào)的手持信號(hào)源輸出的信號(hào)可能質(zhì)量欠佳;另一方面,相比于“BNC——同軸線——SMA”信道,“鱷魚夾——杜邦線——排針”信道的明顯劣勢(shì)也是不言而喻的。 一定程度上減弱抖動(dòng)影響的措施,唯有通過定時(shí)器自帶的數(shù)字濾波器,對(duì)AWD輸出信號(hào)進(jìn)行數(shù)字濾波。但實(shí)驗(yàn)證明,若用2Msps的速率采集峰峰值3.0V的正弦波,即使采用最大濾波長(zhǎng)度,依然會(huì)將10Hz誤測(cè)成100Hz左右,而在濾波前,誤測(cè)值高達(dá)2kHz左右。由于后續(xù)AUTO功能的需要,測(cè)量頻率和數(shù)據(jù)采集是分開的。也即頻率測(cè)量與時(shí)基無關(guān)。
4. 如何計(jì)算信號(hào)頻譜if((!(cursor_buf & (0X1 << 7))) || ((cursor_buf & (0X1 << 7)) && (single_flag == 0)))
//測(cè)量頻率
{
//配置參數(shù)
//保存TIM2原參數(shù),并設(shè)為2MHz采樣率
arr = TIM2->ARR;
TIM2->ARR = 16 - 1;
smp = (ADC1->SMPR) & 0X7;
ADC1->SMPR &= ~(0X7);
ADC1->SMPR |= 0X1;
psc = TIM2->PSC;
TIM2->PSC = 0;
//將TIM1的從模式更改為External Clock 1
//并打開數(shù)字濾波
TIM1->SMCR &= ~((0X1 << 16) | 0X7);
TIM1->SMCR |= 0X7;
TIM1->SMCR |= 0XF << 8;
TIM1->PSC = 0;
TIM1->ARR = 65535;
TIM1->CNT = 0;
TIM1->EGR |= 0X1;
//配置SysTick
SysTick->VAL = 0;
SysTick->LOAD = 16000000 -1;
//開啟測(cè)量
TIM2->CR1 |= 0X1;
TIM1->CR1 |= 0X1;
SysTick->CTRL |= 0X1;
while(!(SysTick_UE_FLAG & 0X1))
{
}
//結(jié)束測(cè)量,恢復(fù)TIM1參數(shù)
TIM1->CR1 &= ~(0X1);
TIM2->CR1 &= ~(0X1);
SysTick_UE_FLAG &= ~(0X1);
TIM1->SMCR &= ~((0X1 << 16) | 0X7);
TIM1->SMCR &= ~(0XF << 8);
}
本項(xiàng)目的FFT算法沒有調(diào)用任何除C++標(biāo)準(zhǔn)庫以外的庫,這一方面是考慮到RAM空間的緊張,另一方面則是起到鍛煉的作用。 本項(xiàng)目的FFT算法就是最簡(jiǎn)單的256點(diǎn)基-2 FFT算法,將復(fù)數(shù)乘法拆分為實(shí)虛部進(jìn)行同址運(yùn)算,并將FFT因子存儲(chǔ)為const型常量。
基-2 FFT的蝶形算子概念在DSP教材中均有解釋,本項(xiàng)目完全依照其定義與原理編寫。以上完成了采樣處理模塊的論述,下面將進(jìn)行GUI模塊的論述。 鑒于本項(xiàng)目具有一定的復(fù)雜性,我們將GUI模塊又分為兩部分:一是用戶交互部分,即按鍵和旋鈕及與之相關(guān)的中斷服務(wù)函數(shù),二是顯示部分,即OLED屏驅(qū)動(dòng)以及主循環(huán)。為了避免使程序過于復(fù)雜,用戶交互部分并不能直接、即時(shí)地改變顯示部分,用戶的操作將被保存在由幾個(gè)變量模擬成的寄存器的各個(gè)位里,并被主循環(huán)的固定部分重復(fù)讀取、刷新。各寄存器及其各位的定義如下。
各個(gè)位的含義及位置,均以宏定義的形式在頭文件中聲明。這樣,就可以在刷新函數(shù)中通過位運(yùn)算的方式獲取各個(gè)參數(shù)。
這樣做的顯著好處就是極大地節(jié)省了RAM空間。因?yàn)樽钚〉淖兞恳彩?位,卻沒有任何參數(shù)達(dá)到256檔之多,尤其是那些只有一位的標(biāo)志位,完全沒有必要用8位變量表示。當(dāng)然,這又是一對(duì)用時(shí)間換空間的矛盾。因?yàn)槲贿\(yùn)算的操作量是直接賦值運(yùn)算的3倍,這是在內(nèi)存空間緊張的情況下最好的選擇。//macros for register ui_buf
5. 如何以盡可能低的誤判率獲取按鍵和旋鈕的信息
由硬件電路可知,旋鈕的AB相、旋鈕按鍵、左右按鍵,分別連接在PB4,PA15,PB3,PA4,PA5上。其中,三個(gè)按鍵只要用外部中斷+延時(shí)消抖就能很好地判斷,而旋鈕則具有一定的復(fù)雜性。
我們判斷旋鈕不應(yīng)選擇上升沿,這是由旋鈕的硬件特性決定的。出于簡(jiǎn)化考慮,本項(xiàng)目只對(duì)PA15的下降沿做了外部中斷,即:根據(jù)下降沿時(shí)PB4的電平高低來判斷左旋或右旋,但這帶來的問題也很明顯:如果旋鈕被誤轉(zhuǎn)了一半,那么即使松開復(fù)原了,也會(huì)被判定為一次轉(zhuǎn)動(dòng)——這往往發(fā)生在用戶完成一次有效轉(zhuǎn)動(dòng)之后,由于慣性而導(dǎo)致的誤觸。 事實(shí)上,正確的做法應(yīng)該是:用TIM3的CC1來捕捉PB4(以此避開與PA4在EXTI Line4上的沖突),用EXTI Line15來捕捉PA15。只要兩個(gè)中斷服務(wù)函數(shù)共享一個(gè)全局變量,就可以解決誤觸的問題。 由下圖(在下一頁)可以看出,除了切換主副界面以外,按鍵和旋鈕并不會(huì)直接去動(dòng)那6個(gè)全局變量寄存器。而主副界面的“切換”也只是動(dòng)了一個(gè)位M_S_FLAG,真正的顯示更新在主循環(huán)中完成。除此之外,按鍵和旋鈕的加、減被記錄在變量add_buf和min_buf中,而因?yàn)榘存I和旋鈕都可以進(jìn)行加減操作,因此用flag寄存器的0位和7位來表示究竟是按鍵按下,還是旋鈕轉(zhuǎn)動(dòng)。為了避免抖動(dòng),在PA15外部中斷時(shí),add_buf和min_buf只有一個(gè)能被置位,而置位它的同時(shí)將強(qiáng)行清零另一個(gè),也算是一個(gè)簡(jiǎn)單的軟件消抖。這其實(shí)也回答了需求中提出的第二個(gè)問題:中斷服務(wù)函數(shù)只改變加減標(biāo)志位,而不改變?nèi)旨拇嫫?,否則整個(gè)服務(wù)函數(shù)將因充斥各種邏輯判斷而變得十分冗長(zhǎng)與龐大,以至于喧賓奪主。
6. 如何以盡可能簡(jiǎn)潔的方式實(shí)現(xiàn)按鍵對(duì)GUI的改變void EXTI4_15_IRQHandler(void)
{
if(EXTI->FPR1 & (0X1 << 15))
{
flag |= 0X1 << 7;
if(!(GPIOB->IDR & (0X1 << 4)))
{
add_buf ++;
min_buf = 0;
}
else
{
min_buf ++;
add_buf = 0;
}
TIM17_Delay(5000-1,320-1);
EXTI->FPR1 |= 0X1 << 15;
}
if(EXTI->FPR1 & (0X1 << 4))//left key down,--, or switch to main ui
{
TIM17_Delay(5000-1,320-1);
if(!(GPIOA->IDR & (0X1 << 4)))
{
flag |= 0X1;
if(GPIOB->IDR & (0X1 << 3))//PB3 not down
{
if(!(M_S_FLAG & cursor_buf))//main ui
{
if(!(ui_buf & FFT_ON_BIT))
{
if((cursor_buf & M_UI_BITS) > 0)
cursor_buf -= 0X1 << M_UI_BITS_OFFSET;
}
else
{
fft_col |= 0X1 << 7;//變量標(biāo)志位
}
}
else//sub ui
{
if((cursor_buf & S_UI_BITS) > 0)
cursor_buf -= 0X1 << S_UI_BITS_OFFSET;
}
}
else//PB3 down
{
cursor_buf &= ~(M_S_FLAG);
}
}
EXTI->FPR1 |= 0X1 << 4;
}
if(EXTI->FPR1 & (0X1 << 5))//right key down,++, or switch to sub ui
{
TIM17_Delay(5000-1,320-1);
if(!(GPIOA->IDR & (0X1 << 5)))
{
flag |= 0X1;
if(GPIOB->IDR & (0X1 << 3))//PB3 not down
{
if(!(M_S_FLAG & cursor_buf))//main ui
{
if(!(ui_buf & FFT_ON_BIT))
{
if((cursor_buf & M_UI_BITS) < (0X4 << M_UI_BITS_OFFSET))
cursor_buf += 0X1 << M_UI_BITS_OFFSET;
}
else
{
fft_col &= ~(0X1 << 7);
}
}
else//sub ui
{
if((cursor_buf & S_UI_BITS) < (0X4 << S_UI_BITS_OFFSET))
cursor_buf += 0X1 << S_UI_BITS_OFFSET;
}
}
else//PB3 down
{
cursor_buf |= M_S_FLAG;
}
}
EXTI->FPR1 |= 0X1 << 5;
}
__NVIC_ClearPendingIRQ(EXTI4_15_IRQn);
}
由上述討論可以看出,最簡(jiǎn)潔的方式就是在每次進(jìn)入主循環(huán)后的固定位置,根據(jù)6個(gè)全局寄存器的值,共同決定本次循環(huán)應(yīng)該在屏幕上顯示什么,并清除所有的標(biāo)志位。由于實(shí)現(xiàn)該功能的UI_Refresh函數(shù)太長(zhǎng),這里僅以一個(gè)switch-case分支作為示例。
目至今沒有完全得出優(yōu)化解的另一個(gè)難點(diǎn)。雖然這樣的結(jié)構(gòu)很簡(jiǎn)潔,但我們后續(xù)就將看到:這種完全“同步”于主循環(huán),而屏蔽任何“異步”帶來的后果,就是當(dāng)水平時(shí)基很大時(shí),整個(gè)程序也會(huì)變得非常緩慢,以至于幾乎進(jìn)入了一種“假死”狀態(tài)。因?yàn)榧词拱聪铝税存I,至少也要等一次主循環(huán)結(jié)束。而在以低的采樣率采集數(shù)十Hz信號(hào)時(shí),連同等待觸發(fā)加256個(gè)采樣點(diǎn)在內(nèi)的時(shí)間,是相當(dāng)可觀的。這啟示我們,中斷服務(wù)函數(shù)應(yīng)該真的具有“中斷”的作用,而不僅僅是完成一個(gè)硬件電路就可以實(shí)現(xiàn)的狀態(tài)機(jī)。至于采樣處理模塊的更新,則與GUI的更新如出一轍:同樣是根據(jù)6個(gè)全局寄存器的值來更新,這樣保證了顯示與實(shí)際相符。只不過這一次更新的是模擬開關(guān)檔位、TIM2溢出頻率,TIM14與TIM16的PWM波占空比等參數(shù)。case (0X1 << M_UI_BITS_OFFSET)://水平分格
{
flag |= 0X1 << 2;
&& ((ui_buf & TIME_BASE_BITS) < (0XF << TIME_BASE_BITS_OFFSET)))
{
add_buf = 0;
ui_buf += (0X1 << TIME_BASE_BITS_OFFSET);
}
else if(min_buf && ((ui_buf & TIME_BASE_BITS) > (0X1 << TIME_BASE_BITS_OFFSET)))
{
min_buf = 0;
ui_buf -= (0X1 << TIME_BASE_BITS_OFFSET);
}
break;
}
事實(shí)上,這是本項(xiàng)
其他功能簡(jiǎn)述
在核心部分以外,以下將對(duì)AUTO,Single以及波形顯示函數(shù)作簡(jiǎn)要的論述。1. AUTO功能
所謂的AUTO功能,是指示波器根據(jù)當(dāng)前被采信號(hào)的直流偏置、峰峰值、頻率等特點(diǎn),自動(dòng)調(diào)節(jié)顯示時(shí)基、觸發(fā)電平、垂直尺度等參數(shù),使得整個(gè)波形盡可能以最大的完整度和占滿率顯示在屏幕上。 在本程序中,頻率的測(cè)定與采樣時(shí)基無關(guān),這對(duì)AUTO的實(shí)現(xiàn)無疑是有利的。而由于輸入端采用了反相放大(衰減)器加同相端直流偏置的方式,而不是在同一端接成加法器,因此直流偏置的概念本身變得模糊。
上圖為輸入端電路。其中Vi為真實(shí)輸入值,Vo為ADC實(shí)際采到的值。據(jù)此,我們可以得出如下映射關(guān)系:
根據(jù)這個(gè)關(guān)系,就能根據(jù)ADC采樣值反推出真實(shí)的電壓。在AUTO時(shí),我們首先將觸發(fā)電平選在屏幕中心(即Xadc = 2048,Vo = 1.65V),然后求出真實(shí)輸入電壓的中值(而不是均值,因?yàn)?,如果輸入的?0%占空的方波,那么中值作為觸發(fā)的效果顯然比均值要好),最后,通過解方程的方式,反推出TIM16或TIM14應(yīng)該輸出的PWM波占空比,就能使波形以中值附近為中心顯示在屏幕上。 AUTO模式不能和FFT模式以及Single模式一起開啟。 每次在副界面打開AUTO后,AUTO指令只會(huì)被執(zhí)行一次。在AUTO后,任何除查看負(fù)時(shí)間(波形模式)和頻標(biāo)(FFT模式)以外的操作均會(huì)解除AUTO。每次要執(zhí)行新一次AUTO,需要切換副界面——保證AUTO處于OFF——再將AUTO調(diào)至ON。
2. Single單次模式
在打開Single模式時(shí),示波器會(huì)在一次觸發(fā)之后將波形凍結(jié)。此時(shí)可以切換主副界面,在頻譜和波形顯示之間切換、查看負(fù)時(shí)間(波形模式),以及調(diào)整頻標(biāo)查看各分量大?。‵FT模式)。除此之外的任何操作都會(huì)解除凍結(jié),并自動(dòng)等待與捕捉下一次觸發(fā)。Single模式不能和AUTO模式一起開啟。
3. 波形顯示函數(shù)
與大多數(shù)人不同,本項(xiàng)目的波形顯示函數(shù)沒有調(diào)用DrawLine,而是用了自己編寫的另一個(gè)基于底層的方法。這樣做的初衷是為了進(jìn)一步驗(yàn)證自己對(duì)OLED底層驅(qū)動(dòng)的理解,并試圖通過自己編寫的顯示函數(shù)來避免移植庫中顯存的使用。然而事實(shí)證明,顯存的存在有其優(yōu)勢(shì),且自己建立一套字模就好比天方夜譚。 盡管在8KB RAM的開發(fā)板上,2KB的顯存不免奢侈,但顯存的概念本身——尤其是在緩存以避免頻閃上——是很重要的。對(duì)于一些更高階的開發(fā)板(如F407)系列,顯存將被外擴(kuò)SRAM硬件實(shí)現(xiàn)。一個(gè)典型的例子就是EMWIN庫。 本程序采用的函數(shù),主要是討論一種底層驅(qū)動(dòng)的方法。 繪制波形的確可以用DrawLine,然而也可以采用不同的思路。 因?yàn)椴ㄐ我欢ㄊ且韵噜弮蓚€(gè)點(diǎn)為步進(jìn),一個(gè)一個(gè)點(diǎn)繪制的,也就是說,這本質(zhì)上不是一個(gè)通用的DrawLine問題,而是一個(gè)x軸步進(jìn)固定為1的特殊的DrawLine問題,那么這個(gè)問題就可以有不同的解法。我們可以認(rèn)為:第i點(diǎn)與第i+1點(diǎn)的數(shù)值,共同決定了第i+1列的顯示。但它們不能影響第i列的顯示。 這要從我們調(diào)用的底層講起。板載的這款OLED有兩種尋址模式:一是寫入0X20指令后的列自增尋址,即選定頁地址和列地址后連續(xù)寫入,頁地址固定而行地址自增;二是寫入0X21指令后的頁自增尋址,即選定頁地址和列地址后連續(xù)寫入,頁地址自增而行地址固定。波形繪制使用的就是不同于常規(guī)的0X21指令。 可以想象,每次更新波形時(shí),是一列一列進(jìn)行的。先清除一列上已有的波形,再顯示新的波形(注意這是直接寫進(jìn)OLED里,而不是顯存里的,因此無法進(jìn)行“ |= ”運(yùn)算)。如果第i點(diǎn)和第i+1點(diǎn)共同決定第i列和第i+1列的顯示,那么同理,第i-1點(diǎn)和第i點(diǎn)也將共同決定第i-1列和第i列的顯示。這樣就會(huì)導(dǎo)致第i列在顯示上出現(xiàn)矛盾:后面的會(huì)把前面的沖掉。一個(gè)典型的例子就是:在顯示方波時(shí),這種方法會(huì)導(dǎo)致所有的沿顯示為空白。因此,要想達(dá)到顯示波形的效果,只需要簡(jiǎn)單地在第i+1列上,填充第i點(diǎn)的行與第i+1點(diǎn)的行之間的全部行就可以了。而在后續(xù)顯示示波器分格的虛線、觸發(fā)電平虛線,以及負(fù)時(shí)間或頻標(biāo)虛線時(shí),只要通過簡(jiǎn)單的位運(yùn)算和或運(yùn)算,在恰當(dāng)?shù)男信c列將虛線的每個(gè)像素點(diǎn)與波形數(shù)據(jù)進(jìn)行“或”運(yùn)算即可。
總結(jié)與思考
原文標(biāo)題:如何使用STM32G031開發(fā)板實(shí)現(xiàn)雙通道示波器-2022年寒假在家練STM32平臺(tái)項(xiàng)目分享(一)
文章出處:【微信公眾號(hào):電子森林】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
-
示波器
+關(guān)注
關(guān)注
113文章
6164瀏覽量
184315 -
參數(shù)
+關(guān)注
關(guān)注
11文章
1754瀏覽量
32043 -
開發(fā)板
+關(guān)注
關(guān)注
25文章
4896瀏覽量
97058
原文標(biāo)題:如何使用STM32G031開發(fā)板實(shí)現(xiàn)雙通道示波器-2022年寒假在家練STM32平臺(tái)項(xiàng)目分享(一)
文章出處:【微信號(hào):xiaojiaoyafpga,微信公眾號(hào):電子森林】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論