引言
寒假练有一款白色的、非常美观的、双通道输入的基于STM32G031的板卡,它可以实现哪些功能呢?示波器、DDS信号发生器、频谱分析仪、失真度测量仪等等。 今天我们来看一位来自南京大学的【电子卷卷怪】同学所做的双通道简易示波器项目,这位同学还帮助多个参加寒假练的同学亲自解决了他们的问题。项目成果概述
本项目使用硬禾课堂STM32G031开发板卡以及STM32CubeIDE开发工具,实现了一个简易的示波器。示波器的各项参数或功能概述如下:
1. 外观项目需求分析
总的来说,本项目可以分为两个大的模块:GUI模块、采样处理模块。其中,相对于程序的主循环而言,采样处理模块是高速的、“同步”的,GUI模块是慢速的、“异步”的。两个模块间既需要并行不悖,又需要互相交换数据。 对于采样处理模块,主要考虑以下4个需求问题:1. ADC可控采样率与切换通道的实现;2. 触发电平的实现,以及负时间显示的实现;3. 如何对频率进行较高精度的测定;4. 如何计算信号频谱; 对于GUI模块,主要考虑以下3个需求问题:1. 如何以尽可能低的误判率获取按键与旋钮的信息;2. 中断服务函数所应干涉的范围;3. 如何以尽可能简洁的方式实现按键对GUI的改变 对于两个模块而言,最核心的问题是:如何在两者之间进行高效的数据传输的同时,避免数据的误判或漏判。核心技术路线
针对“二”中提出的需求,以下同样分两个模块,对项目的技术路线进行完备的论述。鉴于HAL库过于庞大,且本人对项目的理解更偏重于硬件底层,除了HAL_Init,SystemClock_Config,以及与NVIC有关的3个最底层的函数(Priority, Enable, ClearPending)外,其他所有的外设配置代码,均为本人阅读器件手册后编写的寄存器代码。模拟看门狗的配置将在后面说明。这里最关键的,一是必须配置为非连续模式、外部上升沿触发,选择TIM2的TRGO为触发源,并且不能选择ADC为DMA触发源,否则ADC的overwritten特性会迫使软件屡屡清除标志位,以保证DMA Request的持续产生;二是在外部触发时,必须先start。 对DMA端: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;
}
传输数据使用的是通道一。相比于F407等系列,G031引入了DMAMUX的概念,使得几乎所有的外设和一些事件都可以在任意一个DMA通道上产生请求。由于DMAMUX的0~4对应DMA的1~5,查阅用户指南后,得知设置DMAMUX的CCR的低7位为31(0X1F)表示TIM2的Update。 对TIM端: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;
}
通过CR2的主模式位MMS[6:4]配置TIM2的Update为TRGO,否则无法正确触发ADC;使能更新事件的DMA请求。 在上述框架下,DMA只要开启单次模式,等待全传输中断函数置标志位就可以了。需要注意的是,在清除中断标志的时候,需要同时清除NVIC端和外设端的标志位,否则会陷入无限的中断循环。 若开启了上述外设配置,则上述架构在DMA One shot模式下就能完成采样率可调的循环数据传输。而我们最终开启的是DMA Circular模式,这将在后面说明。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);
}
3. 信号频率的测定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);
}
4. 如何计算信号频谱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);
}
这样做的显著好处就是极大地节省了RAM空间。因为最小的变量也是8位,却没有任何参数达到256档之多,尤其是那些只有一位的标志位,完全没有必要用8位变量表示。当然,这又是一对用时间换空间的矛盾。因为位运算的操作量是直接赋值运算的3倍,这是在内存空间紧张的情况下最好的选择。//macros for register ui_buf
6. 如何以尽可能简洁的方式实现按键对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;//变量标志位
}
}
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);
}
目至今没有完全得出优化解的另一个难点。虽然这样的结构很简洁,但我们后续就将看到:这种完全“同步”于主循环,而屏蔽任何“异步”带来的后果,就是当水平时基很大时,整个程序也会变得非常缓慢,以至于几乎进入了一种“假死”状态。因为即使按下了按键,至少也要等一次主循环结束。而在以低的采样率采集数十Hz信号时,连同等待触发加256个采样点在内的时间,是相当可观的。这启示我们,中断服务函数应该真的具有“中断”的作用,而不仅仅是完成一个硬件威廉希尔官方网站 就可以实现的状态机。至于采样处理模块的更新,则与GUI的更新如出一辙:同样是根据6个全局寄存器的值来更新,这样保证了显示与实际相符。只不过这一次更新的是模拟开关档位、TIM2溢出频率,TIM14与TIM16的PWM波占空比等参数。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;
}
事实上,这是本项
其他功能简述
在核心部分以外,以下将对AUTO,Single以及波形显示函数作简要的论述。
总结与思考
原文标题:如何使用STM32G031开发板实现双通道示波器-2022年寒假在家练STM32平台项目分享(一)
文章出处:【微信公众号:电子森林】欢迎添加关注!文章转载请注明出处。
全部0条评论
快来发表一下你的评论吧 !