引言
寒假练有一款白色的、非常美观的、双通道输入的基于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。void ADC_init(void){uint32_t temp;RCC->IOPENR |= 0X1UL;//打开PortA时钟temp=RCC->IOPENR;//时钟使能需等2个周期UNUSED(temp);//避免Warning//由于GPIOA->MODER对应位默认为0X3,即模拟输入//因此不需要再额外配置PortARCC->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 configurationADC1->CFGR2 |= (0X3UL<<30); //PCLK as ADC_CLKADC1->CHSELR |= (0X1UL << 1 | 0X0UL<<7);//选择通道一do{temp=ADC1->ISR;}while(!(temp & (0X1UL<<13)));//等待通道配置有效ADC1->CR |= 0X1UL;//enabling ADC1do{temp = ADC1->ISR;}while(!(temp & 0X1UL));//ADC ReadyADC1->CR |= 0X1UL<<2;//ADC Startreturn;}

对DMA端:
传输数据使用的是通道一。相比于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);//避免WarningDMA1_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 channelreturn;}
通过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 TRGOTIM2->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);}
并且,这个信号是硬件连接(hardwired)至TIM1的外部触发MUX的。它可以通过TIM1的AF1寄存器被选择为TIM1的从模式外部触发信号。


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 DMATIM2->CNT = 0;//resetting TIM2dat_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;//配置SysTickSysTick->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条评论
快来发表一下你的评论吧 !