如何使用STM32G031开发板实现双通道示波器

引言✦
寒假练有一款白色的、非常美观的、双通道输入的基于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 |= (0x1ulcr |= (0x1ultr1 |= (0x0fff0800); //模拟看门狗的高低阈值 adc1->cfgr1 |= (0x1<<26 | 0x1<<22 | 0x1ulisr; }while(!(temp & 0x1ul));//adc ready adc1->cr |= 0x1ulahbenr;//时钟使能需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 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 |= 0x1ulpsc = 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 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 |= (0x0ulsr & 0x1ul) { { tim2->cr1 &= ~(0x1ul); tim1->cr1 &= ~(0x1ul); //stop tim2 and consequently stop dma tim2->cnt = 0;//resetting tim2 dat_buf_ready |= 0x1 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 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 smcr |= 0xf 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  在核心部分以外,以下将对auto,single以及波形显示函数作简要的论述。  1. auto功能所谓的auto功能,是指示波器根据当前被采信号的直流偏置、峰峰值、频率等特点,自动调节显示时基、触发电平、垂直尺度等参数,使得整个波形尽可能以最大的完整度和占满率显示在屏幕上。  在本程序中,频率的测定与采样时基无关,这对auto的实现无疑是有利的。而由于输入端采用了反相放大(衰减)器加同相端直流偏置的方式,而不是在同一端接成加法器,因此直流偏置的概念本身变得模糊。上图为输入端电路。其中vi为真实输入值,vo为adc实际采到的值。据此,我们可以得出如下映射关系:根据这个关系,就能根据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模式时,示波器会在一次触发之后将波形冻结。此时可以切换主副界面,在频谱和波形显示之间切换、查看负时间(波形模式),以及调整频标查看各分量大小(fft模式)。除此之外的任何操作都会解除冻结,并自动等待与捕捉下一次触发。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——这是我在本次寄存器编程中唯一一个尚未解释清楚的问题。


技术详析:直流系统接地故障的危害
NI开发 ActiveUptime™ 维护服务(MaaS),为测试设备和设施提供状态监测和预测性维护解决方案
聚焦“中国芯”!与产业同行 芯华章受邀出席琴珠澳集成电路产业促进峰会
【直播问答精选(下)】虹科《工艺设备验证》主题研讨会——验证从未如此简单!
从富士康看美帝制造业回流
如何使用STM32G031开发板实现双通道示波器
毫米级Delta机器人研制成功,能够完成制造和医药领域的一系列微操作任务
浅析Knuth高效洗牌算法
Atmosic Technologies与Energous实现业界首例互操作性能量收集,推动无线充电应用发展
如何利用Kinect控制51单片机
Mini LED需求增大给全自动点胶机带来怎样的机遇
芯讯通、龙尚科技CEO杨涛出席Ayla战略发布会
全球半导体市场下滑超两位数几无悬念 芯片厂商凭啥给出增长预期?
迅为STM32MP157开发板入门教程之外设功能验证
为什么单片机的晶振旁边要加电容呢?
火爆的区块链到底可以做一些什么
铁锂电池再受市场关注,安全性是最大优势
Redmi K30 5G网络实测 下载速度达949Mbps
过孔导电滑环安装方法
一种以三个芯片级联而成的窄脉冲小信号运算放大电路