学习单片机,通过做简单的小项目,是成长最快的一种方法。今天就给大家分享一个制作简易示波器的小项目,代码开源,希望对初学者有帮助。
一、前言
该项目是基于正点原子精英板制作的一个简易示波器,可以读取信号的频率和幅值,并可以通过按键改变采样频率和控制屏幕的更新暂停。
二、硬件接线
将pa6与pa4相连,可观察到正弦波。
将pa6与pa5相连,可观察到三角波/噪声(默认三角波)。
key_up控制波形的更新和暂停。
key_1降低采样率。
key_0提高采样率。
三、信号的采集
信号的采集主要是依靠adc(通过定时器触发采样,与在定时器中断中开启一次采样的效果类似,以此来控制采样的间隔时间相同),然后通过dma将所采集的数据从adc的dr寄存器转移到一个变量中,此时完成一次采样。
由于设定采集一次完整的波形需要1024个点,即需要连续采集1024次才算一次完整的波形采样(需要采集1024个点的原因在后面会提到)。
因此我们还需创建一个数组用于存储这些数据,并在dma中断中,将成功转移到变量中的数据依次存储进数组(注意此数组中存入的数据是12位的数字量,还未做回归处理),完成1024个数据的采样和储存,用于后续在lcd上进行波形的显示和相关参数的处理。
此案例用到的是adc1的通道6(即pa6口)进行数据的采样,主要需注意将adc转换的触发方式改为定时器触发(我用的是定时器2的通道2进行触发,由于stm32手册提示只有在上升沿时可以触发adc,因此我们需要让定时器2的通道2每隔固定的时间产生一个上升沿)。
将定时器2设置成pwm模式,即可令adc1在定时器2的通道2每产生一次上升沿时触发采样,后续即可通过改变pwm的频率(即定时器的溢出频率),便可控制采样的频率。
四、代码配置
adc的配置:
/**********************************************************简介:adc1-ch6初始化函数***********************************************************/ void adc_init(void){ adc_inittypedef adc_initstructure; gpio_inittypedef gpio_initstructure; rcc_apb2periphclockcmd(rcc_apb2periph_gpioa |rcc_apb2periph_adc1, enable ); //使能adc1通道时钟 rcc_adcclkconfig(rcc_pclk2_div6); //设置adc分频因子6 72m/6=12,adc最大时间不能超过14m //pa6 作为模拟通道输入引脚 gpio_initstructure.gpio_pin = gpio_pin_6; gpio_initstructure.gpio_mode = gpio_mode_ain; //模拟输入 gpio_initstructure.gpio_speed = gpio_speed_50mhz; gpio_init(gpioa, &gpio_initstructure); adc_deinit(adc1); //复位adc1,将外设 adc1 的全部寄存器重设为缺省值 adc_initstructure.adc_mode = adc_mode_independent; //adc工作模式:adc1工作在独立模式 adc_initstructure.adc_scanconvmode = disable; //模数转换工作在单通道模式 adc_initstructure.adc_continuousconvmode = disable; //模数转换工作在非连续转换模式 adc_initstructure.adc_externaltrigconv = adc_externaltrigconv_t2_cc2; //转换由定时器2的通道2触发(只有在上升沿时可以触发) adc_initstructure.adc_dataalign = adc_dataalign_right; //adc数据右对齐 adc_initstructure.adc_nbrofchannel = 1; //顺序进行规则转换的adc通道的数目 adc_init(adc1, &adc_initstructure); //根据adc_initstruct中指定的参数初始化外设adcx的寄存器 adc_cmd(adc1, enable); //使能指定的adc1 adc_dmacmd(adc1, enable); //adc的dma功能使能 adc_resetcalibration(adc1); //使能复位校准 adc_regularchannelconfig(adc1, adc_channel_6, 1, adc_sampletime_1cycles5 );//adc1通道6,采样时间为239.5周期 adc_resetcalibration(adc1);//复位较准寄存器 while(adc_getresetcalibrationstatus(adc1)); //等待复位校准结束 adc_startcalibration(adc1); //开启ad校准 while(adc_getcalibrationstatus(adc1)); //等待校准结束 adc_softwarestartconvcmd(adc1, enable); //使能指定的adc1的软件转换启动功能}
定时器的配置:
/******************************************************************函数名称:tim2_pwm_init(u16 arr,u16 psc)函数功能:定时器3,pwm输出模式初始化函数参数说明:arr:重装载值 psc:预分频值备 注:通过tim2-ch2的pwm输出触发adc采样*******************************************************************/ void tim2_pwm_init(u16 arr,u16 psc){ gpio_inittypedef gpio_initstructure; tim_timebaseinittypedef tim_timebasestructure; tim_ocinittypedef tim_ocinitstructure; rcc_apb1periphclockcmd(rcc_apb1periph_tim2, enable); //使能定时器2时钟 rcc_apb2periphclockcmd(rcc_apb2periph_gpioa | rcc_apb2periph_afio, enable); //使能gpio外设和afio复用功能模块时钟 //设置该引脚为复用输出功能,输出tim2 ch2的pwm脉冲波形 gpioa.1 gpio_initstructure.gpio_pin = gpio_pin_1; //tim_ch2 gpio_initstructure.gpio_mode = gpio_mode_af_pp; //复用推挽输出 gpio_initstructure.gpio_speed = gpio_speed_50mhz; gpio_init(gpioa, &gpio_initstructure);//初始化gpio //初始化tim3 tim_timebasestructure.tim_period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 tim_timebasestructure.tim_prescaler =psc; //设置用来作为timx时钟频率除数的预分频值 tim_timebasestructure.tim_clockdivision = 0; //设置时钟分割:tdts = tck_tim tim_timebasestructure.tim_countermode = tim_countermode_up; //tim向上计数模式 tim_timebaseinit(tim2, &tim_timebasestructure); //根据tim_timebaseinitstruct中指定的参数初始化timx的时间基数单位 //初始化tim2 channel2 pwm模式 tim_ocinitstructure.tim_ocmode = tim_ocmode_pwm1; //选择定时器模式:tim脉冲宽度调制模式2 tim_ocinitstructure.tim_outputstate = tim_outputstate_enable; //比较输出使能 tim_ocinitstructure.tim_ocpolarity = tim_ocpolarity_low; //输出极性:tim输出比较极性高 tim_ocinitstructure.tim_pulse=1000; //发生反转时的计数器数值,用于改变占空比 tim_oc2init(tim2, &tim_ocinitstructure); //根据t指定的参数初始化外设tim2 tim_ctrlpwmoutputs(tim2, enable);//使能pwm输出 tim_cmd(tim2, enable); //使能tim2}
dma配置:
/******************************************************************函数名称:mydma1_config()函数功能:dma1初始化配置参数说明:dma_chx:dma通道选择 cpar:dma外设adc基地址 cmar:dma内存基地址 cndtrdma通道的dma缓存的大小备 注:*******************************************************************/void mydma1_config(dma_channel_typedef* dma_chx,u32 cpar,u32 cmar,u16 cndtr){ dma_inittypedef dma_initstructure; nvic_inittypedef nvic_initstructure; rcc_ahbperiphclockcmd(rcc_ahbperiph_dma1, enable); //使能dma传输 dma_deinit(dma_chx); //将dma的通道1寄存器重设为缺省值 dma_initstructure.dma_peripheralbaseaddr = cpar; //dma外设adc基地址 dma_initstructure.dma_memorybaseaddr = cmar; //dma内存基地址 dma_initstructure.dma_dir = dma_dir_peripheralsrc; //数据传输方向,从外设读取发送到内存// dma_initstructure.dma_buffersize = cndtr; //dma通道的dma缓存的大小 dma_initstructure.dma_peripheralinc = dma_peripheralinc_disable; //外设地址寄存器不变 dma_initstructure.dma_memoryinc = dma_memoryinc_enable; //内存地址寄存器递增 dma_initstructure.dma_peripheraldatasize = dma_peripheraldatasize_halfword; //数据宽度为16位 dma_initstructure.dma_memorydatasize = dma_memorydatasize_halfword; //数据宽度为16位 dma_initstructure.dma_mode = dma_mode_circular; //工作在循环模式 dma_initstructure.dma_priority = dma_priority_high; //dma通道 x拥有高优先级 dma_initstructure.dma_m2m = dma_m2m_disable; //dma通道x没有设置为内存到内存传输 dma_init(dma_chx, &dma_initstructure); //adc1匹配dma通道1 dma_itconfig(dma1_channel1,dma1_it_tc1,enable); //使能dma传输中断 //配置中断优先级 nvic_initstructure.nvic_irqchannel = dma1_channel1_irqn; nvic_initstructure.nvic_irqchannelpreemptionpriority=0 ; nvic_initstructure.nvic_irqchannelsubpriority = 0; nvic_initstructure.nvic_irqchannelcmd = enable; nvic_init(&nvic_initstructure); dma_cmd(dma1_channel1,enable);//使能dma通道}
注意:
由于在设置pwm时将tim_pulse默认设置为1000,因此在初始化定时器2时,tim_period的值不能小于该值,可自行修改。tim_pulse的值并不会影响采样频率。
采样频率= 定时器2溢出频率=sysclk/预分频值/溢出值因此如果将tim_pulse设为1,tim_period设为2,tim_prescaler设为1,理论上采样频率最高可达36mhz。
五、数据的处理
数据的处理主要是要求出信号的频率和幅值等相关参数。幅值可以通过找出之前存储1024个点的数组中最大最小值,回归处理过后算出差值。
难点主要在于频率的求取。一个信号中可能包含多种频率成分,而我显示的是幅值最大的频率分量(当然其他频率也可获得)。这里便用到了stm32提供的dsp库中的fft(快速傅里叶变换),dsp库在最后的源码中有。
需要采样1024个点的原因:fft算法要求样本数为2的n次方,而dsp库中提供了64,256和1024样本数对应的库函数,因此选用1024最大样本数可以使频率分辨率最小,更加精确。(定义频率分辨率f0=fs/n,其中fs等于采样率,n为采样点数)
需注意:fft后的输出不是实际的信号频率,需要经过转换。f(k)=k*(fs/n),其中f(k)是实际频率,k是实际信号的最大幅度频率所对应的数。(详见下面代码,分享的源代码中公式有误,未重新上传)
获取频率的函数:
#define npt 1024//一次完整采集的采样点数/******************************************************************函数名称:getpowermag()函数功能:计算各次谐波幅值参数说明:备 注:先将lbufoutarray分解成实部(x)和虚部(y),然后计算幅值(sqrt(x*x+y*y)*******************************************************************/void getpowermag(void){ float x,y,mag,magmax;//实部,虚部,各频率幅值,最大幅值 u16 i; //调用自cr4_fft_1024_stm32 cr4_fft_1024_stm32(fftout, fftin, npt); //fftin为傅里叶输入序列数组,ffout为傅里叶输出序列数组 for(i=1; i
> 16); mag = sqrt(x * x + y * y); fft_mag[i]=mag;//存入缓存,用于输出查验 //获取最大频率分量及其幅值 if(mag > magmax) { magmax = mag; temp = i; } } f=(u16)(temp*(fre*1.0/npt));//源代码中此公式有误,将此复制进去 lcd_shownum(280,180,f,5,16);}
六、模拟正弦波输出
此正弦波输出是用于调试示波器,观察显示和实际是否相同。主要利用dac输出,在定时器3的中断中不断改变dac的输出值,产生一个正弦波。因此改变正弦波的频率可以通过更改定时器3的溢出频率。(采用的pa4口进行输出)
在初始化时,我将定时器3的重装载值设置为40,预分频值设置为72,正弦波输出频率为72mhz/40/72/1024≈24.5hz(1024是因为将一个周期正弦波均分成1024个输出点,详见下面函数initbufinarray())。
经采样处理后显示为24-25hz,与实际值接近。(但是当采样频率提高到最大3.6khz时,频率显示为32hz左右,原因未知)
下面是相关代码:
u16 magout[npt];/******************************************************************函数名称:initbufinarray()函数功能:正弦波值初始化,将正弦波各点的值存入magout[]数组中参数说明:备 注:*******************************************************************/void initbufinarray(void){ u16 i; float fx; for(i=0; i=npt) i=0;}
七、模拟噪声或三角波输出
模拟噪声或三角波输出可直接通过配置dac,利用芯片内部的发生器产生。dac2的转换由定时器4的trgo触发(事件触发)。同时需要注意设置trgo由更新事件产生。
若为三角波输出,频率=72mhz/定时器重装载值/预分频系数/幅值/2;
例如:初始化定时器的重装载值为2,预分频系数为36,幅值为最大(4096),即freq=72mhz/2/36/4096/2≈122hz;
具体代码如下所示:
void dac2_init(void){ gpio_inittypedef gpio_initstructure; dac_inittypedef dac_inittype; rcc_apb2periphclockcmd(rcc_apb2periph_gpioa, enable ); //使能porta通道时钟 rcc_apb1periphclockcmd(rcc_apb1periph_dac, enable ); //使能dac通道时钟 gpio_initstructure.gpio_pin = gpio_pin_5; // 端口配置 gpio_initstructure.gpio_mode = gpio_mode_ain; //模拟输入 gpio_initstructure.gpio_speed = gpio_speed_50mhz; gpio_init(gpioa, &gpio_initstructure); dac_inittype.dac_trigger=dac_trigger_t4_trgo; //定时器4触发 dac_inittype.dac_wavegeneration=dac_wavegeneration_noise;//产生噪声 //dac_wavegeneration_triangle产生三角波 dac_inittype.dac_lfsrunmask_triangleamplitude = dac_triangleamplitude_4095;//幅值设置为最大,即3.3v dac_inittype.dac_outputbuffer=dac_outputbuffer_disable ; //dac1输出缓存关闭 boff1=1 dac_init(dac_channel_2,&dac_inittype); //初始化dac通道2 dac_cmd(dac_channel_2, enable); //使能dac-ch2 dac_setchannel1data(dac_align_12b_r, 0); //12位右对齐数据格式设置dac值 }void tim4_int_init(u16 arr,u16 psc){ tim_timebaseinittypedef tim_timebasestructure; rcc_apb1periphclockcmd(rcc_apb1periph_tim4, enable); //时钟使能 tim_timebasestructure.tim_period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 计数到5000为500ms tim_timebasestructure.tim_prescaler =psc; //设置用来作为timx时钟频率除数的预分频值 10khz的计数频率 tim_timebasestructure.tim_clockdivision = 0; //设置时钟分割:tdts = tck_tim tim_timebasestructure.tim_countermode = tim_countermode_up; //tim向上计数模式 tim_timebaseinit(tim4, &tim_timebasestructure); //根据tim_timebaseinitstruct中指定的参数初始化timx的时间基数单位 tim_selectoutputtrigger(tim4, tim_trgosource_update);//触发外设方式为更新触发 tim_cmd(tim4, enable); //使能timx外设 }
八、显示函数与按键控制
显示波形只需将所获得的1024个采样数据选择一部分进行显示大致思路如下:
u16 pre_vol;//当前电压值对应点的纵坐标u16 past_vol;//前一个电压值对应点的纵坐标//adcx[]数组及通过dma存入的1024个原始数据pre_vol = 50+adcx[x]/4096.0*100;lcd_drawline(x,past_vol,x+1,pre_vol);//根据实际,打点位置可进行相应更改past_vol = pre_vol;
按键的控制是在外部中断中进行(正点原子资料中提供相应参考代码)比较重要的是改变采样频率。
工程分享
https://gitee.com/silent-rookie/simple-oscilloscope
PCB上的单端阻抗控制50欧姆的原因是什么
苹果汽车推出日期接近2030年
移动互联网的快速发展,显示屏的生产合格率非常重要的一环
以实例分析上拉电阻
优势挑战并存,商用网络产业链共推5G毫米波商业化
基于单片机的简易示波器设计
汽车真的缺芯片吗 汽车芯片短缺分析
小米无人机对比精灵3s拆解 大疆“门徒”表现如何?
NEPCON China电子展正式加入UFI行列!
环境监控云平台,扬尘监测仪实现实时数据查询
锂电池电动车的使用与保养
智能垃圾桶存在哪一些优点和缺点
智能手表天生就有两种属性
中兴通讯携手合作伙伴助力运营商实现5G商业成功
5G移动网的融和服务正在为媒体融合向纵深发展提供有力支撑
非洲猪瘟实验室检测设备的产品特点介绍
iPhone13粉色款3分钟秒没,iPhone13首批售罄连夜补货
《中国互联网发展报告(2020)》发布
看权威人如何士分析华为压制三星成为苹果的强敌
台湾智能手机和PC大厂华硕正在实施大规模裁员计划