<acronym id="s8ci2"><small id="s8ci2"></small></acronym>
<rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
<acronym id="s8ci2"></acronym>
<acronym id="s8ci2"><center id="s8ci2"></center></acronym>
0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

基于STM32G031開發板的雙通道簡易示波器設計

電子森林 ? 來源:電子森林 ? 作者:電子森林 ? 2022-03-22 09:13 ? 次閱讀

引言

寒假練有一款白色的、非常美觀的、雙通道輸入的基于STM32G031的板卡,它可以實現哪些功能呢?示波器、DDS信號發生器、頻譜分析儀、失真度測量儀等等。 今天我們來看一位來自南京大學的【電子卷卷怪】同學所做的雙通道簡易示波器項目,這位同學還幫助多個參加寒假練的同學親自解決了他們的問題。

項目成果概述

本項目使用硬禾課堂STM32G031開發板卡以及STM32CubeIDE開發工具,實現了一個簡易的示波器。示波器的各項參數或功能概述如下:

1. 外觀

(1)有主界面、副界面兩個界面,并可以相互切換;

(2)主界面包含波形模式和FFT模式,分別顯示被測信號的波形和頻譜;

(3)波形模式包含:垂直尺度調整、水平時基調整、屏幕中心電平調整、模擬觸發電平調整、負時間調整、平均值顯示、頻率測量顯示、峰峰值顯示;FFT模式包含:垂直尺度顯示、采樣率顯示、屏幕中心電平顯示、模擬觸發電平顯示、頻譜最大分量(歸一化值)顯示、頻標調整、頻標對應分量顯示。

(4)副界面包含5個其他功能:通道選擇、波形/FFT模式切換、開啟AUTO、模擬觸發電平極性、開啟單次(Single)模式。

2. 操作

(1)位于主界面的任意模式時,單擊左右鍵可以使光標在該模式下可調整的功能間移動,轉動旋鈕調整被光標選中的參數;

(2)位于副界面時,單擊左右鍵可以使光標在5個其他功能間移動,轉動旋鈕可以調整被選中的功能;

(3)按住旋鈕的情況下:單擊左鍵進入主界面、單擊右鍵進入副界面。

(4)開啟AUTO后,自適應調整只會在切回主界面后被執行一次;對新波形的自適應調整需要切到副界面——開啟AUTO——切回主界面。

(5)開啟Single后,無觸發時,正常顯示波形;觸發一次后,波形與頻譜均固定,并不會更新,但可以調整負時間和頻標;在觸發后,調整垂直尺度、水平時基、屏幕中心電平、模擬觸發電平、采樣率中的任意一者,都會導致下一次觸發的捕捉。80301cb6-a917-11ec-952b-dac502259ad0.jpg8044a082-a917-11ec-952b-dac502259ad0.jpg80598f9c-a917-11ec-952b-dac502259ad0.jpg ?

項目需求分析

總的來說,本項目可以分為兩個大的模塊:GUI模塊、采樣處理模塊。其中,相對于程序的主循環而言,采樣處理模塊是高速的、“同步”的,GUI模塊是慢速的、“異步”的。兩個模塊間既需要并行不悖,又需要互相交換數據。 對于采樣處理模塊,主要考慮以下4個需求問題:1. ADC可控采樣率與切換通道的實現;2. 觸發電平的實現,以及負時間顯示的實現;3. 如何對頻率進行較高精度的測定;4. 如何計算信號頻譜; 對于GUI模塊,主要考慮以下3個需求問題:1. 如何以盡可能低的誤判率獲取按鍵與旋鈕的信息;2. 中斷服務函數所應干涉的范圍;3. 如何以盡可能簡潔的方式實現按鍵對GUI的改變 對于兩個模塊而言,最核心的問題是:如何在兩者之間進行高效的數據傳輸的同時,避免數據的誤判或漏判。

核心技術路線

針對“二”中提出的需求,以下同樣分兩個模塊,對項目的技術路線進行完備的論述。鑒于HAL庫過于龐大,且本人對項目的理解更偏重于硬件底層,除了HAL_Init,SystemClock_Config,以及與NVIC有關的3個最底層的函數(Priority, Enable, ClearPending)外,其他所有的外設配置代碼,均為本人閱讀器件手冊后編寫的寄存器代碼。

1. ADC可控采樣率與通道切換

在ADC連續模式下,雖然可以通過調整采樣時間來調整采樣率,但這樣做顯然并不好。一方面,這樣得到的轉換周期(Tsamp + 12.5ADC_Cycle)的倒數,即頻率,往往是不規律的非整數,這樣做不利于功能調整的層次化與統一化;另一方面,即使采用16MHz主頻,在12位分辨率下,ADC最小轉化頻率也有16MHz / (160.5 + 12.5) ≈92.5kHz,有效測量范圍太小。 定時器觸發的方式是最好的選擇。一方面,只需控制轉換時間不大于采樣率的倒數,就能獲得完全可控的轉換率;另一方面,這樣有利于定時器觸發DMA傳輸的引入。由于在32MHz主頻下,即使是最簡單的中斷服務函數,頻率也只能到150kHz左右,因此,DMA傳輸既可以提供較高的采樣率,又可以使“采樣——處理”分離的結構更加清晰。配置的方法: 對ADC端:

	
void ADC_init(void){  uint32_t temp;  RCC->IOPENR |= 0X1UL;//打開PortA時鐘  temp=RCC->IOPENR;//時鐘使能需等2個周期  UNUSED(temp);//避免Warning  //由于GPIOA->MODER對應位默認為0X3,即模擬輸入  //因此不需要再額外配置PortA
  RCC->APBENR2 |= (0X1UL<<20UL);//打開ADC1時鐘  temp=RCC->APBENR2;  UNUSED(temp);  ADC1->CR |= (0X1UL<<28UL);//使能內部參考電壓  //自己寫的延時,用TIM17的OPM模式  TIM17_Delay(1000-1,32-1);//等待參考電壓有效  ADC1->CR |= (0X1UL<<31UL);  do  {    temp=ADC1->CR;//開始校正指令  }while(temp & (0X1UL<<31));//等待校正結束  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;}
模擬看門狗的配置將在后面說明。這里最關鍵的,一是必須配置為非連續模式、外部上升沿觸發,選擇TIM2的TRGO為觸發源,并且不能選擇ADC為DMA觸發源,否則ADC的overwritten特性會迫使軟件屢屢清除標志位,以保證DMA Request的持續產生;二是在外部觸發時,必須先start。806cdcc8-a917-11ec-952b-dac502259ad0.jpg80807a4e-a917-11ec-952b-dac502259ad0.jpg ?對DMA端:

	
void ADC_DMA_init(void){  uint32_t temp;  RCC->AHBENR |= 0X1UL;  temp=RCC->AHBENR;//時鐘使能需2個周期  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;}
傳輸數據使用的是通道一。相比于F407等系列,G031引入了DMAMUX的概念,使得幾乎所有的外設和一些事件都可以在任意一個DMA通道上產生請求。由于DMAMUX的0~4對應DMA的1~5,查閱用戶指南后,得知設置DMAMUX的CCR的低7位為31(0X1F)表示TIM2的Update。 對TIM端:

	
void TIM2_Init(unsigned int priority){  uint32_t temp;  RCC->APBENR1 |= 0X1UL;//使能TIM2時鐘  temp=RCC->APBENR1;  UNUSED(temp);  //TIM2->DIER |= 0X1UL;//允許更新中斷  TIM2->CR1 |= 0X1UL<<2UL;//手動更新不觸發中斷  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;//手動更新寄存器值  temp=TIM2->PSC;  UNUSED(temp);}
通過CR2的主模式位MMS[6:4]配置TIM2的Update為TRGO,否則無法正確觸發ADC;使能更新事件的DMA請求。 在上述框架下,DMA只要開啟單次模式,等待全傳輸中斷函數置標志位就可以了。需要注意的是,在清除中斷標志的時候,需要同時清除NVIC端和外設端的標志位,否則會陷入無限的中斷循環。 若開啟了上述外設配置,則上述架構在DMA One shot模式下就能完成采樣率可調的循環數據傳輸。而我們最終開啟的是DMA Circular模式,這將在后面說明。

2. 觸發電平的實現,以及負時間的實現

觸發電平,即以被測信號越過某個閾值電壓為起算點,采集后面的若干個數據。該方法可以使波形穩定地顯示在屏幕上。 負時間,即可以顯示觸發電平前一定時間內的波形。當觸發電平用于異常信號的單次捕捉(Single模式)時,負時間可以顯示異常信號前的波形。 有同學在無條件采樣后計算一組數據的均值(中值),并顯示從中值樣點開始的數據,從而通過軟件實現觸發電平。這種方案在實現AUTO時不失為一個好的啟發,但在此面臨兩個問題:第一,單純的中值判斷無法控制觸發的極性,即無法選擇上升沿還是下降沿觸發。若增加前后值判斷,則將增加軟件運算量;第二,這種算法下不可能出現“無觸發”的、波形亂晃的現象,與真實的數字示波器存在差異。從本質上講,這種方法沒有充分利用硬件底層。 G031的ADC自帶一個模擬看門狗,即Analog Window Watchdog的特性。即當采樣值超出規定范圍(窗口)時,輸出AWD_OUT將持續拉高,直至電壓落回窗口內,延遲為一個轉換周期。8092b4ac-a917-11ec-952b-dac502259ad0.jpg并且,這個信號是硬件連接(hardwired)至TIM1的外部觸發MUX的。它可以通過TIM1的AF1寄存器被選擇為TIM1的從模式外部觸發信號。80a518e0-a917-11ec-952b-dac502259ad0.jpg ?

配置TIM1從模式為Trigger Mode(上升沿觸發啟動)、選擇觸發源為外部觸發ETR,再連接AWD1至ETR,就可以在DMA One Shot模式下,實現基于硬件的、真正的觸發電平功能。通過ADC的TR1設置閾值,假設TIM1為上升沿啟動,則當窗口為(x , 0x0FFF)時,為下降沿觸發;當窗口為(0x0000 , x)時,為上升沿觸發。
80b64160-a917-11ec-952b-dac502259ad0.jpg

然而在這樣的結構下,是無法實現負時間功能的。由于AWD_OUT的上升沿是不可預知的隨機事件,因此應該對程序結構進行微調:改用DMA Circular模式,AWD_OUT作為采樣停止——而不是開始——的信號。 假如我們希望采集觸發后的256個數據(為方便FFT運算),又希望顯示負時間的128個數據,則應該配置TIM2為ADC觸發源,令TIM1的溢出周期為TIM2的256倍。在TIM1的中斷服務函數中關掉(Disable)TIM2,就能實現上述功能。與此同時,DMA1_Channel1的CNDTR中將保存一個循環中剩余待傳輸的數據個數,據此可以定位連同負時間在內的整段有效數據在DMA目標數組內的起止位置。

80c4e1e8-a917-11ec-952b-dac502259ad0.jpg ?

若目標數組大小為512,當TIM2停止時,CNDTR的值為CH1_CNDTR,則觸發點下標應為(512 - CH1_CNDTR - 256) % 512= (512 - CH1_CNDTR + 256) % 512= (768 - CH1_CNDTR) % 512 然而這樣的設計存在一個問題:模擬觸發事件具有隨機性,如果它在重新開啟TIM2后的幾個周期內就發生,那么當新一段數據被存儲完成后,負時間位置的數據還是上次采樣的數據,這就會導致負時間顯示錯誤。

80d7dd52-a917-11ec-952b-dac502259ad0.jpg


為了避免上述情況,在新一輪開啟后,必須先等待一次全傳輸中斷再開啟TIM1。事實上,只要一次全傳輸中斷后,無論TIM1隔多久開啟,數組中的時間軸都是連續的。用dat_buf_ready的bit0表示全傳輸中斷、bit7表示TIM1中斷。

	
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;//賦值了不一定用,但這樣最準確  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)//中斷服務函數{  DMA1->IFCR |=0X1UL;  dat_buf_ready |= 0X1;  __NVIC_ClearPendingIRQ(DMA1_Channel1_IRQn);}
3. 信號頻率的測定


數字測定頻率的方法,一般是先整形再測量。即通過施密特觸發器(比如TLV3501)先把信號整形成脈沖,再對脈沖進行測定。對脈沖的測定也有兩種思路:一是直接同步采樣后計算脈沖個數,適用于較高頻率;二是計算脈沖高低電平的周期個數,適用于較低頻率。兩種方法均受限于系統最高主頻。這也是本項目至今為止兩個尚為得出最優解的難點之一。 本項目從脈沖整形到計數均采用硬件特性為主、軟件程序為輔的思路。根據前面的討論可知,ADC的AWD在一定頻率以下等效于一個極其理想的脈沖整形器。相較于模擬施密特觸發器,其最大的特點在于脈沖整形的響應特性與信號峰峰值的絕對值無關,而僅受到信道噪聲和量化噪聲的干擾。因此,測量頻率最基本的方法,也是本項目采用的方法,就是對AWD的輸出信號AWD_OUT在一定時間內進行計數。此方法實現起來最為簡單,但面臨兩個很大的問題:第一,相比于FPGA廣泛采用的雙閘門法,此方法會把閘門時間的前后沿漏掉,引入一定的誤差,但這并非主要矛盾。80ec4530-a917-11ec-952b-dac502259ad0.jpg


第二,實測表明,在測定較低頻率的正弦波或三角波時,頻率將出現較大誤差,只有對方波的測定最為準確。這種誤差只有在200Hz以上才可以忽略不計。究其本質,是因為信號的噪聲抖動所致。AWD_OUT的靈敏度帶來了一個致命的缺點:沒有任何的滯回特性,這就導致在過觸發點附近的任何噪聲都可能被極大地放大,只有邊沿極抖的方波才能“幸免”。反觀模擬脈沖整形電路,由于人為設計滯回電路以及電路本身輸入輸出電容的存在,對輸入信號總有一定的消抖能力。當然,用于傳輸測試信號的信道本身也存在問題。一方面,用于輸出測試信號的手持信號源輸出的信號可能質量欠佳;另一方面,相比于“BNC——同軸線——SMA”信道,“鱷魚夾——杜邦線——排針”信道的明顯劣勢也是不言而喻的。 一定程度上減弱抖動影響的措施,唯有通過定時器自帶的數字濾波器,對AWD輸出信號進行數字濾波。但實驗證明,若用2Msps的速率采集峰峰值3.0V的正弦波,即使采用最大濾波長度,依然會將10Hz誤測成100Hz左右,而在濾波前,誤測值高達2kHz左右。由于后續AUTO功能的需要,測量頻率和數據采集是分開的。也即頻率測量與時基無關。

	
if((!(cursor_buf & (0X1 << 7))) || ((cursor_buf & (0X1 << 7)) && (single_flag == 0)))//測量頻率        {       //配置參數        //保存TIM2原參數,并設為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        //并打開數字濾波        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;
        //開啟測量        TIM2->CR1 |= 0X1;        TIM1->CR1 |= 0X1;        SysTick->CTRL |= 0X1;        while(!(SysTick_UE_FLAG & 0X1))        {        }        //結束測量,恢復TIM1參數        TIM1->CR1 &= ~(0X1);        TIM2->CR1 &= ~(0X1);        SysTick_UE_FLAG &= ~(0X1);        TIM1->SMCR &= ~((0X1 << 16) | 0X7);        TIM1->SMCR &= ~(0XF << 8);      }
4. 如何計算信號頻譜


本項目的FFT算法沒有調用任何除C++標準庫以外的庫,這一方面是考慮到RAM空間的緊張,另一方面則是起到鍛煉的作用。 本項目的FFT算法就是最簡單的256點基-2 FFT算法,將復數乘法拆分為實虛部進行同址運算,并將FFT因子存儲為const型常量。


81047ab0-a917-11ec-952b-dac502259ad0.jpg


基-2 FFT的蝶形算子概念在DSP教材中均有解釋,本項目完全依照其定義與原理編寫。以上完成了采樣處理模塊的論述,下面將進行GUI模塊的論述。 鑒于本項目具有一定的復雜性,我們將GUI模塊又分為兩部分:一是用戶交互部分,即按鍵和旋鈕及與之相關的中斷服務函數,二是顯示部分,即OLED屏驅動以及主循環。為了避免使程序過于復雜,用戶交互部分并不能直接、即時地改變顯示部分,用戶的操作將被保存在由幾個變量模擬成的寄存器的各個位里,并被主循環的固定部分重復讀取、刷新。各寄存器及其各位的定義如下。811bca12-a917-11ec-952b-dac502259ad0.jpg


各個位的含義及位置,均以宏定義的形式在頭文件中聲明。這樣,就可以在刷新函數中通過位運算的方式獲取各個參數。

	
//macros for register ui_buf#define CH_SEL_BIT_OFFSET (0)#define CH_SEL_BIT (0X1 << CH_SEL_BIT_OFFSET)
#define FFT_ON_BIT_OFFSET (1)#define FFT_ON_BIT (0X1 << FFT_ON_BIT_OFFSET)
#define TRG_POL_BIT_OFFSET (2)#define TRG_POL_BIT (0X1 << TRG_POL_BIT_OFFSET)
#define AUTO_ON_BIT_OFFSET (3)#define AUTO_ON_BIT (0X1 << AUTO_ON_BIT_OFFSET)
#define TIME_BASE_BITS_OFFSET (4)#define TIME_BASE_BITS (0XF << TIME_BASE_BITS_OFFSET)
#define AMP_DIV_BITS_OFFSET (8)#define AMP_DIV_BITS (0XF << AMP_DIV_BITS_OFFSET)
#define NEG_TIME_BITS_OFFSET (12)#define NEG_TIME_BITS (0X7F << NEG_TIME_BITS_OFFSET)
#define TRG_LV_BITS_OFFSET (19)#define TRG_LV_BITS (0X7F << TRG_LV_BITS_OFFSET)
#define BIAS_BITS_OFFSET (26)#define BIAS_BITS (0X3F << BIAS_BITS_OFFSET)
這樣做的顯著好處就是極大地節省了RAM空間。因為最小的變量也是8位,卻沒有任何參數達到256檔之多,尤其是那些只有一位的標志位,完全沒有必要用8位變量表示。當然,這又是一對用時間換空間的矛盾。因為位運算的操作量是直接賦值運算的3倍,這是在內存空間緊張的情況下最好的選擇。


5. 如何以盡可能低的誤判率獲取按鍵和旋鈕的信息


由硬件電路可知,旋鈕的AB相、旋鈕按鍵、左右按鍵,分別連接在PB4,PA15,PB3,PA4,PA5上。其中,三個按鍵只要用外部中斷+延時消抖就能很好地判斷,而旋鈕則具有一定的復雜性。

81345ac8-a917-11ec-952b-dac502259ad0.jpg

我們判斷旋鈕不應選擇上升沿,這是由旋鈕的硬件特性決定的。出于簡化考慮,本項目只對PA15的下降沿做了外部中斷,即:根據下降沿時PB4的電平高低來判斷左旋或右旋,但這帶來的問題也很明顯:如果旋鈕被誤轉了一半,那么即使松開復原了,也會被判定為一次轉動——這往往發生在用戶完成一次有效轉動之后,由于慣性而導致的誤觸。 事實上,正確的做法應該是:用TIM3的CC1來捕捉PB4(以此避開與PA4在EXTI Line4上的沖突),用EXTI Line15來捕捉PA15。只要兩個中斷服務函數共享一個全局變量,就可以解決誤觸的問題。 由下圖(在下一頁)可以看出,除了切換主副界面以外,按鍵和旋鈕并不會直接去動那6個全局變量寄存器。而主副界面的“切換”也只是動了一個位M_S_FLAG,真正的顯示更新在主循環中完成。除此之外,按鍵和旋鈕的加、減被記錄在變量add_buf和min_buf中,而因為按鍵和旋鈕都可以進行加減操作,因此用flag寄存器的0位和7位來表示究竟是按鍵按下,還是旋鈕轉動。為了避免抖動,在PA15外部中斷時,add_buf和min_buf只有一個能被置位,而置位它的同時將強行清零另一個,也算是一個簡單的軟件消抖。這其實也回答了需求中提出的第二個問題:中斷服務函數只改變加減標志位,而不改變全局寄存器,否則整個服務函數將因充斥各種邏輯判斷而變得十分冗長與龐大,以至于喧賓奪主。

	
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;//變量標志位        }      }      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);}
6. 如何以盡可能簡潔的方式實現按鍵對GUI的改變


由上述討論可以看出,最簡潔的方式就是在每次進入主循環后的固定位置,根據6個全局寄存器的值,共同決定本次循環應該在屏幕上顯示什么,并清除所有的標志位。由于實現該功能的UI_Refresh函數太長,這里僅以一個switch-case分支作為示例。

	
case (0X1 << M_UI_BITS_OFFSET)://水平分格{  flag |= 0X1 << 2;  if(add_buf && ((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;}事實上,這是本項
目至今沒有完全得出優化解的另一個難點。雖然這樣的結構很簡潔,但我們后續就將看到:這種完全“同步”于主循環,而屏蔽任何“異步”帶來的后果,就是當水平時基很大時,整個程序也會變得非常緩慢,以至于幾乎進入了一種“假死”狀態。因為即使按下了按鍵,至少也要等一次主循環結束。而在以低的采樣率采集數十Hz信號時,連同等待觸發加256個采樣點在內的時間,是相當可觀的。這啟示我們,中斷服務函數應該真的具有“中斷”的作用,而不僅僅是完成一個硬件電路就可以實現的狀態機。至于采樣處理模塊的更新,則與GUI的更新如出一轍:同樣是根據6個全局寄存器的值來更新,這樣保證了顯示與實際相符。只不過這一次更新的是模擬開關檔位、TIM2溢出頻率,TIM14與TIM16的PWM波占空比等參數。

其他功能簡述

在核心部分以外,以下將對AUTO,Single以及波形顯示函數作簡要的論述。


1. AUTO功能

所謂的AUTO功能,是指示波器根據當前被采信號的直流偏置、峰峰值、頻率等特點,自動調節顯示時基、觸發電平、垂直尺度等參數,使得整個波形盡可能以最大的完整度和占滿率顯示在屏幕上。 在本程序中,頻率的測定與采樣時基無關,這對AUTO的實現無疑是有利的。而由于輸入端采用了反相放大(衰減)器加同相端直流偏置的方式,而不是在同一端接成加法器,因此直流偏置的概念本身變得模糊。

81450a12-a917-11ec-952b-dac502259ad0.jpg

上圖為輸入端電路。其中Vi為真實輸入值,Vo為ADC實際采到的值。據此,我們可以得出如下映射關系:

815b95ca-a917-11ec-952b-dac502259ad0.jpg


根據這個關系,就能根據ADC采樣值反推出真實的電壓。在AUTO時,我們首先將觸發電平選在屏幕中心(即Xadc = 2048,Vo = 1.65V),然后求出真實輸入電壓的中值(而不是均值,因為,如果輸入的是90%占空的方波,那么中值作為觸發的效果顯然比均值要好),最后,通過解方程的方式,反推出TIM16或TIM14應該輸出的PWM波占空比,就能使波形以中值附近為中心顯示在屏幕上。 AUTO模式不能和FFT模式以及Single模式一起開啟。 每次在副界面打開AUTO后,AUTO指令只會被執行一次。在AUTO后,任何除查看負時間(波形模式)和頻標(FFT模式)以外的操作均會解除AUTO。每次要執行新一次AUTO,需要切換副界面——保證AUTO處于OFF——再將AUTO調至ON。


2. Single單次模式


在打開Single模式時,示波器會在一次觸發之后將波形凍結。此時可以切換主副界面,在頻譜和波形顯示之間切換、查看負時間(波形模式),以及調整頻標查看各分量大?。‵FT模式)。除此之外的任何操作都會解除凍結,并自動等待與捕捉下一次觸發。Single模式不能和AUTO模式一起開啟。

3. 波形顯示函數


與大多數人不同,本項目的波形顯示函數沒有調用DrawLine,而是用了自己編寫的另一個基于底層的方法。這樣做的初衷是為了進一步驗證自己對OLED底層驅動的理解,并試圖通過自己編寫的顯示函數來避免移植庫中顯存的使用。然而事實證明,顯存的存在有其優勢,且自己建立一套字模就好比天方夜譚。 盡管在8KB RAM的開發板上,2KB的顯存不免奢侈,但顯存的概念本身——尤其是在緩存以避免頻閃上——是很重要的。對于一些更高階的開發板(如F407)系列,顯存將被外擴SRAM硬件實現。一個典型的例子就是EMWIN庫。 本程序采用的函數,主要是討論一種底層驅動的方法。 繪制波形的確可以用DrawLine,然而也可以采用不同的思路。 因為波形一定是以相鄰兩個點為步進,一個一個點繪制的,也就是說,這本質上不是一個通用的DrawLine問題,而是一個x軸步進固定為1的特殊的DrawLine問題,那么這個問題就可以有不同的解法。我們可以認為:第i點與第i+1點的數值,共同決定了第i+1列的顯示。但它們不能影響第i列的顯示。 這要從我們調用的底層講起。板載的這款OLED有兩種尋址模式:一是寫入0X20指令后的列自增尋址,即選定頁地址和列地址后連續寫入,頁地址固定而行地址自增;二是寫入0X21指令后的頁自增尋址,即選定頁地址和列地址后連續寫入,頁地址自增而行地址固定。波形繪制使用的就是不同于常規的0X21指令。 可以想象,每次更新波形時,是一列一列進行的。先清除一列上已有的波形,再顯示新的波形(注意這是直接寫進OLED里,而不是顯存里的,因此無法進行“ |= ”運算)。如果第i點和第i+1點共同決定第i列和第i+1列的顯示,那么同理,第i-1點和第i點也將共同決定第i-1列和第i列的顯示。這樣就會導致第i列在顯示上出現矛盾:后面的會把前面的沖掉。一個典型的例子就是:在顯示方波時,這種方法會導致所有的沿顯示為空白。因此,要想達到顯示波形的效果,只需要簡單地在第i+1列上,填充第i點的行與第i+1點的行之間的全部行就可以了。而在后續顯示示波器分格的虛線、觸發電平虛線,以及負時間或頻標虛線時,只要通過簡單的位運算和或運算,在恰當的行與列將虛線的每個像素點與波形數據進行“或”運算即可。



總結與思考

由于此前完全沒有獨立進行過這種完整項目的開發,更沒有試過完全擺脫庫函數的束縛,直接對著器件手冊進行寄存器編程,因此從這個意義上講,這次項目實踐的收獲無疑是豐碩的。 寄存器編程的巨大好處不僅僅在于證明自己有不再跟著別人編寫的庫函數亦步亦趨的能力,而更多地在于對單片機的硬件特性本身有了更加深入和透徹的理解。單是覺得看懂了器件手冊卻不會根據自己對它的理解進行編程,抑或單是學會用庫函數拼湊出各種功能而不知其所以然,在我看來都不是學習嵌入式最終的目的。換句話說,我既不想像“嵌入式接口技術”課那樣,花一個學期背完一份考綱,也不想把嵌入式開發玩成一個純軟件的東西。 這其中當然也摻雜著很大的個人因素。在參加電賽的一年半里,我已經受夠了在那些被奉為神諭的庫函數面前不得不唯唯諾諾的姿態,為此我還我曾經干出過這樣的蠢事:每次新建一個工程,就把所有相關的外設驅動庫函數復制一份過去,以至于銳減的E盤容量呈現出一派日積月累和勤學苦練的虛假繁榮——全都是泡沫經濟罷了。 性格執拗與固執行事,就好比在眾目睽睽之下丟開大路不走,而非要在泥潭水溝里摔得鼻青臉腫,比的就是在他人的冷嘲熱諷與自己的自我懷疑中,哪個會先讓你破防。別人看了視頻,用一行代碼一分鐘就點亮的燈,我卻花了半天還差點沒累死在電腦前,換來的是兩個刻骨銘心的知識點:配置外設前要先打開RCC的時鐘位,以及GPIO MODER的復位值是全1而不是全0。當綠燈亮起來的那一刻,我才深切地意識到,自己還有很長的路需要走。 我的另一個教訓在于低估了GUI設計的難度。在2月9號完成了核心部分的架構之后,我天真地以為只要享受接下來的過程就好了,殊不知行百里者半九十,真正魔鬼的東西,恰恰還在后頭。時至今日,我雖然好歹結了個項,但心中的滿足簡直微乎其微。程序跑出來的整體結果很不理想,單純的實現功能與不惡心你的用戶之間,隔的真不是一兩條鴻溝。在敲完代碼后,我才意識到是否應該引入一個簡單的操作系統來實現一個更加人性化的GUI,因為挫敗使我意識到:這種“睜眼主循環,閉眼進中斷”的思維,可能確實有點太單細胞了。 除此之外,實現一個示波器所需的各種基本功能,我也有了一個大體的理解。知識決定想象力的范疇,這與我一貫的認知相符——畢竟在此之前,我也只限于紙上談兵地構建一款擁有各種功能示波器,卻每每止步于“這個功能理論上可以實現”。然而當我試圖把自己的想法變為現實,理論和實際的差距才會一次次地刷新我的認知:別人說起來輕描淡寫的東西,始終都只是別人的。 后續的改進我不會在這個項目的基礎上完成,而會在一個去年遺留下來的產學合作項目中,轉移到F407和G031構成的雙MCU結構(我都沒臉說“雙核系統”,真的)上完成。至于這個平臺,它還有實驗價值。我還得用它來搞清楚,為什么用外部時鐘驅動計時器時,溢出更新無法觸發DMA——這是我在本次寄存器編程中唯一一個尚未解釋清楚的問題。

原文標題:如何使用STM32G031開發板實現雙通道示波器-2022年寒假在家練STM32平臺項目分享(一)

文章出處:【微信公眾號:電子森林】歡迎添加關注!文章轉載請注明出處。

審核編輯:湯梓紅
聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 示波器
    +關注

    關注

    111

    文章

    5664

    瀏覽量

    181825
  • 參數
    +關注

    關注

    11

    文章

    1398

    瀏覽量

    31482
  • 開發板
    +關注

    關注

    25

    文章

    4441

    瀏覽量

    94122

原文標題:如何使用STM32G031開發板實現雙通道示波器-2022年寒假在家練STM32平臺項目分享(一)

文章出處:【微信號:xiaojiaoyafpga,微信公眾號:電子森林】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    stm32G031串口外部不接上拉電阻,導致stm32進入到了硬件中斷怎么解決?

    stm32G031使用串口和另一其他芯片交互,外部直連,未接上拉電阻,導致stm32進入到了硬件中斷
    發表于 03-13 07:59

    stm32g031如何用iar開發?

    stm32g031 如何用iar開發?在iar中沒有看到器件列表支持啊
    發表于 04-08 08:03

    為什么Stm32g031芯片無法進入bootloader狀態呢

    為什么Stm32g031芯片無法進入bootloader狀態呢?為何新的Stm32g031芯片智能使用一次ISP燒寫呢?
    發表于 11-25 06:03

    STM32G031無線溫濕度儀開源

    STM32G031無線溫濕度儀開源項目關鍵詞:CubeMX,CubeIDE,STM32G031C8T6,AHT10,DRF1609H1、項目任務本項目MCU使用STM32G031C8T6,單片機讀取
    發表于 01-07 07:57

    基于stm32 mini開發板簡易示波器的設計資料分享

    基于stm32 mini開發板簡易示波器
    發表于 01-17 09:08

    【國民技術】N32G031開發板資料

    用戶手冊*附件:UM_N32G031 Series User Manual V2.1_CN.pdf勘誤手冊*附件:ES_N32G031 Series Errate Sheet V1.1_CN.pdf開發板原理圖及PCB*附件:5
    發表于 10-31 15:34

    N32G031C8L7_STB開發板硬件使用指南

    N32G031C8L7_STB開發板用于國民技術股份有限公司高性能 32 位 N32G031C8L7 系列芯片的樣片開發。本文檔詳細描述了 N32G
    發表于 11-02 07:44

    【國民技術】N32G031開發板資料

    ].pdf用戶手冊*附件:UM_N32G031_Series_User_Manual_V2.1_CN[1].pdf勘誤手冊*附件:ES_N32G031_Series_Errate_Sheet_V1.1_CN[1].pdf開發板
    發表于 11-03 14:37

    使用STM32G031運行CRC計算但結果不匹配是哪里出問題了

    我正在嘗試使用 STM32G031 運行 CRC 計算,但結果不匹配。但我得到 0x277F。你可以在下面找到我的實現。unsigned short CRC_16Bit_Polynom
    發表于 12-13 06:14

    是否可以將14.7456MHz晶體與STM32G031 (LQFP32) 一起使用?

    您好,是否可以將 14.7456MHz 晶體與 STM32G031 (LQFP32) 一起使用。如果我是對的,在數據??表中是不可能的,這個包沒有 OSC_OUT。還是數據表中缺少該選項?我們需要這個晶體用于 IO-Link 應用。
    發表于 12-30 07:25

    基于FPGA的雙通道簡易可存儲示波器設計

    基于FPGA的雙通道簡易可存儲示波器設計:本文介紹了一種基于FPGA的采樣速度60Mbit/s的雙通道簡易數字
    發表于 09-29 10:45 ?109次下載

    基于stm32mini開發板簡易函數發生器和簡易示波器

    基于stm32 mini開發板簡易示波器
    發表于 01-17 10:01 ?35次下載
    基于<b class='flag-5'>stm32</b>mini<b class='flag-5'>開發板</b>的<b class='flag-5'>簡易</b>函數發生器和<b class='flag-5'>簡易</b><b class='flag-5'>示波器</b>

    如何使用STM32G031開發板實現雙通道示波器

    開啟Single后,無觸發時,正常顯示波形;觸發一次后,波形與頻譜均固定,并不會更新,但可以調整負時間和頻標;在觸發后,調整垂直尺度、水平時基、屏幕中心電平、模擬觸發電平、采樣率中的任意一者,都會導致下一次觸發的捕捉。
    的頭像 發表于 03-22 09:57 ?2794次閱讀

    STM32G031J6開發板

    電子發燒友網站提供《STM32G031J6開發板.zip》資料免費下載
    發表于 08-02 15:22 ?15次下載
    <b class='flag-5'>STM32G031</b>J6<b class='flag-5'>開發板</b>

    N32G031C8L7_STB開發板

    N32G031C8L7_STB開發板
    發表于 11-10 19:51 ?0次下載
    N32G<b class='flag-5'>031</b>C8L7_STB<b class='flag-5'>開發板</b>
    亚洲欧美日韩精品久久_久久精品AⅤ无码中文_日本中文字幕有码在线播放_亚洲视频高清不卡在线观看
    <acronym id="s8ci2"><small id="s8ci2"></small></acronym>
    <rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
    <acronym id="s8ci2"></acronym>
    <acronym id="s8ci2"><center id="s8ci2"></center></acronym>