引言
寒假练有一款白色的、非常美观的、双通道输入的基于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后,无触发时,正常显示波形;触发一次后,波形与频谱均固定,并不会更新,但可以调整负时间和频标;在触发后,调整垂直尺度、水平时基、屏幕中心电平、模拟触发电平、采样率中的任意一者,都会导致下一次触发的捕捉。
项目需求分析
总的来说,本项目可以分为两个大的模块: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。对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将持续拉高,直至电压落回窗口内,延迟为一个转换周期。并且,这个信号是硬件连接(hardwired)至TIM1的外部触发MUX的。它可以通过TIM1的AF1寄存器被选择为TIM1的从模式外部触发信号。
配置TIM1从模式为Trigger Mode(上升沿触发启动)、选择触发源为外部触发ETR,再连接AWD1至ETR,就可以在DMA One Shot模式下,实现基于硬件的、真正的触发电平功能。通过ADC的TR1设置阈值,假设TIM1为上升沿启动,则当窗口为(x , 0x0FFF)时,为下降沿触发;当窗口为(0x0000 , x)时,为上升沿触发。
然而在这样的结构下,是无法实现负时间功能的。由于AWD_OUT的上升沿是不可预知的随机事件,因此应该对程序结构进行微调:改用DMA Circular模式,AWD_OUT作为采样停止——而不是开始——的信号。 假如我们希望采集触发后的256个数据(为方便FFT运算),又希望显示负时间的128个数据,则应该配置TIM2为ADC触发源,令TIM1的溢出周期为TIM2的256倍。在TIM1的中断服务函数中关掉(Disable)TIM2,就能实现上述功能。与此同时,DMA1_Channel1的CNDTR中将保存一个循环中剩余待传输的数据个数,据此可以定位连同负时间在内的整段有效数据在DMA目标数组内的起止位置。
若目标数组大小为512,当TIM2停止时,CNDTR的值为CH1_CNDTR,则触发点下标应为(512 - CH1_CNDTR - 256) % 512= (512 - CH1_CNDTR + 256) % 512= (768 - CH1_CNDTR) % 512 然而这样的设计存在一个问题:模拟触发事件具有随机性,如果它在重新开启TIM2后的几个周期内就发生,那么当新一段数据被存储完成后,负时间位置的数据还是上次采样的数据,这就会导致负时间显示错误。
为了避免上述情况,在新一轮开启后,必须先等待一次全传输中断再开启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广泛采用的双闸门法,此方法会把闸门时间的前后沿漏掉,引入一定的误差,但这并非主要矛盾。
第二,实测表明,在测定较低频率的正弦波或三角波时,频率将出现较大误差,只有对方波的测定最为准确。这种误差只有在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;