当前位置:   article > 正文

基于STM32的开源简易示波器项目

基于STM32的开源简易示波器项目

目录

​一、前言

二、硬件接线

三、信号的采集

四、代码配置

五、数据的处理

六、模拟正弦波输出

七、模拟噪声或三角波输出

八、显示函数与按键控制


​一、前言

该项目是基于正点原子精英板制作的一个简易示波器,可以读取信号的频率和幅值,并可以通过按键改变采样频率和控制屏幕的更新暂停。

二、硬件接线

  • 将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的配置:

  1. /**********************************************************
  2. 简介:ADC1-CH6初始化函数
  3. ***********************************************************/                  
  4. void  Adc_Init(void)
  5. {  
  6.  ADC_InitTypeDef ADC_InitStructure; 
  7.  GPIO_InitTypeDef GPIO_InitStructure;
  8.  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1, ENABLE );   //使能ADC1通道时钟
  9.  
  10.  RCC_ADCCLKConfig(RCC_PCLK2_Div6);   //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
  11.  //PA6 作为模拟通道输入引脚                         
  12.  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
  13.  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;  //模拟输入
  14.  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  15.  GPIO_Init(GPIOA, &GPIO_InitStructure); 
  16.  ADC_DeInit(ADC1);  //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值
  17.  ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1工作在独立模式
  18.  ADC_InitStructure.ADC_ScanConvMode = DISABLE; //模数转换工作在单通道模式
  19.  ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //模数转换工作在非连续转换模式
  20.  ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_CC2//转换由定时器2的通道2触发(只有在上升沿时可以触发)
  21.  ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right//ADC数据右对齐
  22.  ADC_InitStructure.ADC_NbrOfChannel = 1//顺序进行规则转换的ADC通道的数目
  23.  ADC_Init(ADC1&ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器   
  24.  ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1
  25.  
  26.  ADC_DMACmd(ADC1, ENABLE); //ADC的DMA功能使能
  27.  
  28.  ADC_ResetCalibration(ADC1); //使能复位校准  
  29.   
  30.  ADC_RegularChannelConfig(ADC1, ADC_Channel_61, ADC_SampleTime_1Cycles5 );//ADC1通道6,采样时间为239.5周期  
  31.   
  32.  ADC_ResetCalibration(ADC1);//复位较准寄存器
  33.   
  34.  while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
  35.  
  36.  ADC_StartCalibration(ADC1);  //开启AD校准
  37.  
  38.  while(ADC_GetCalibrationStatus(ADC1));  //等待校准结束
  39.  
  40.  ADC_SoftwareStartConvCmd(ADC1, ENABLE);  //使能指定的ADC1的软件转换启动功能
  41. }   

定时器的配置:

  1. /******************************************************************
  2. 函数名称:TIM2_PWM_Init(u16 arr,u16 psc)
  3. 函数功能:定时器3,PWM输出模式初始化函数
  4. 参数说明:arr:重装载值
  5.    psc:预分频值
  6. 备    注:通过TIM2-CH2的PWM输出触发ADC采样
  7. *******************************************************************/  
  8. void TIM2_PWM_Init(u16 arr,u16 psc)
  9. {  
  10.  GPIO_InitTypeDef GPIO_InitStructure;
  11.  TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
  12.  TIM_OCInitTypeDef  TIM_OCInitStructure;
  13.  
  14.  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //使能定时器2时钟
  15.   RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA  | RCC_APB2Periph_AFIO, ENABLE);  //使能GPIO外设和AFIO复用功能模块时钟
  16.  
  17.    //设置该引脚为复用输出功能,输出TIM2 CH2的PWM脉冲波形 GPIOA.1
  18.  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1//TIM_CH2
  19.  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出
  20.  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  21.  GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIO
  22.  
  23.    //初始化TIM3
  24.  TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
  25.  TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值 
  26.  TIM_TimeBaseStructure.TIM_ClockDivision = 0//设置时钟分割:TDTS = Tck_tim
  27.  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
  28.  TIM_TimeBaseInit(TIM2&TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
  29.  
  30.  //初始化TIM2 Channel2 PWM模式  
  31.  TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1//选择定时器模式:TIM脉冲宽度调制模式2
  32.   TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
  33.  TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //输出极性:TIM输出比较极性高
  34.  TIM_OCInitStructure.TIM_Pulse=1000//发生反转时的计数器数值,用于改变占空比
  35.  TIM_OC2Init(TIM2&TIM_OCInitStructure);  //根据T指定的参数初始化外设TIM2
  36.  TIM_CtrlPWMOutputs(TIM2, ENABLE);//使能PWM输出
  37.  
  38.  TIM_Cmd(TIM2, ENABLE);  //使能TIM2
  39. }

DMA配置:

  1. /******************************************************************
  2. 函数名称:MYDMA1_Config()
  3. 函数功能:DMA1初始化配置
  4. 参数说明:DMA_CHx:DMA通道选择
  5.    cpar:DMA外设ADC基地址
  6.    cmar:DMA内存基地址
  7.    cndtrDMA通道的DMA缓存的大小
  8. 备    注:
  9. *******************************************************************/
  10. void MYDMA1_Config(DMA_Channel_TypeDef* DMA_CHx,u32 cpar,u32 cmar,u16 cndtr)
  11. {
  12.  DMA_InitTypeDef DMA_InitStructure;
  13.  NVIC_InitTypeDef NVIC_InitStructure;
  14.  
  15.   RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA传输
  16.  
  17.     DMA_DeInit(DMA_CHx);   //将DMA的通道1寄存器重设为缺省值
  18.  DMA_InitStructure.DMA_PeripheralBaseAddr = cpar;  //DMA外设ADC基地址
  19.  DMA_InitStructure.DMA_MemoryBaseAddr = cmar;  //DMA内存基地址
  20.  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;  //数据传输方向,从外设读取发送到内存//
  21.  DMA_InitStructure.DMA_BufferSize = cndtr;  //DMA通道的DMA缓存的大小
  22.  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;  //外设地址寄存器不变
  23.  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  //内存地址寄存器递增
  24.  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;  //数据宽度为16
  25.  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //数据宽度为16
  26.  DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;  //工作在循环模式
  27.  DMA_InitStructure.DMA_Priority = DMA_Priority_High; //DMA通道 x拥有高优先级 
  28.  DMA_InitStructure.DMA_M2= DMA_M2M_Disable;  //DMA通道x没有设置为内存到内存传输
  29.  DMA_Init(DMA_CHx, &DMA_InitStructure);  //ADC1匹配DMA通道1
  30.  
  31.  DMA_ITConfig(DMA1_Channel1,DMA1_IT_TC1,ENABLE); //使能DMA传输中断 
  32.  
  33.  //配置中断优先级
  34.  NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn;
  35.  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0 ;
  36.  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  
  37.  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;   
  38.  NVIC_Init(&NVIC_InitStructure); 
  39.  DMA_Cmd(DMA1_Channel1,ENABLE);//使能DMA通道
  40. }

注意:

  1. 由于在设置PWM时将TIM_Pulse默认设置为1000,因此在初始化定时器2时,TIM_Period的值不能小于该值,可自行修改。TIM_Pulse的值并不会影响采样频率

  2. 采样频率= 定时器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是实际信号的最大幅度频率所对应的数。(详见下面代码,分享的源代码中公式有误,未重新上传)

获取频率的函数:

  1. #define NPT 1024//一次完整采集的采样点数
  2. /******************************************************************
  3. 函数名称:GetPowerMag()
  4. 函数功能:计算各次谐波幅值
  5. 参数说明:
  6. 备  注:先将lBufOutArray分解成实部(X)和虚部(Y),然后计算幅值(sqrt(X*X+Y*Y)
  7. *******************************************************************/
  8. void GetPowerMag(void)
  9. {
  10.     float X,Y,Mag,magmax;//实部,虚部,各频率幅值,最大幅值
  11.     u16 i;
  12.  
  13.  //调用自cr4_fft_1024_stm32
  14.  cr4_fft_1024_stm32(fftout, fftin, NPT); 
  15.  //fftin为傅里叶输入序列数组,ffout为傅里叶输出序列数组
  16.  
  17.     for(i=1; i<NPT/2; i++)
  18.     {
  19.   X = (fftout[i] << 16>> 16;
  20.   Y = (fftout[i] >> 16);
  21.   
  22.   Mag = sqrt(X * X + Y * Y); 
  23.   FFT_Mag[i]=Mag;//存入缓存,用于输出查验
  24.   //获取最大频率分量及其幅值
  25.   if(Mag > magmax)
  26.   {
  27.    magmax = Mag;
  28.    temp = i;
  29.   }
  30.     }
  31.  F=(u16)(temp*(fre*1.0/NPT));//源代码中此公式有误,将此复制进去
  32.  
  33.  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左右,原因未知)

下面是相关代码:

  1. u16 magout[NPT];
  2. /******************************************************************
  3. 函数名称:InitBufInArray()
  4. 函数功能:正弦波值初始化,将正弦波各点的值存入magout[]数组中
  5. 参数说明:
  6. 备    注:
  7. *******************************************************************/
  8. void InitBufInArray(void)
  9. {
  10.     u16 i;
  11.     float fx;
  12.     for(i=0; i<NPT; i++)
  13.     {
  14.         fx = sin((PI2*i)/NPT);
  15.         magout[i] = (u16)(2048+2048*fx);
  16.     }
  17. }
  18. /******************************************************************
  19. 函数名称:sinout()
  20. 函数功能:正弦波输出
  21. 参数说明:
  22. 备    注:将此函数置于定时器中断中,可模拟输出正弦波
  23. *******************************************************************/
  24. void sinout(void)
  25. {
  26.  static u16 i=0;
  27.  DAC_SetChannel1Data(DAC_Align_12b_R,magout[i]);
  28.  i++;
  29.  if(i>=NPT)
  30.   i=0;
  31. }

七、模拟噪声或三角波输出

模拟噪声或三角波输出可直接通过配置DAC,利用芯片内部的发生器产生。DAC2的转换由定时器4的TRGO触发(事件触发)。同时需要注意设置TRGO由更新事件产生。

若为三角波输出,频率=72Mhz/定时器重装载值/预分频系数/幅值/2

例如:初始化定时器的重装载值为2,预分频系数为36,幅值为最大(4096),即Freq=72Mhz/2/36/4096/2≈122Hz

具体代码如下所示:

  1. void Dac2_Init(void)
  2. {
  3.     GPIO_InitTypeDef GPIO_InitStructure;
  4.     DAC_InitTypeDef DAC_InitType;
  5.     RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE );   //使能PORTA通道时钟
  6.     RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE );   //使能DAC通道时钟 
  7.     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;     // 端口配置
  8.     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;    //模拟输入
  9.     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  10.     GPIO_Init(GPIOA, &GPIO_InitStructure);
  11.      
  12.     DAC_InitType.DAC_Trigger=DAC_Trigger_T4_TRGO; //定时器4触发
  13.     DAC_InitType.DAC_WaveGeneration=DAC_WaveGeneration_Noise;//产生噪声
  14.     //DAC_WaveGeneration_Triangle产生三角波
  15.     DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude =  DAC_TriangleAmplitude_4095;//幅值设置为最大,即3.3V
  16.     DAC_InitType.DAC_OutputBuffer=DAC_OutputBuffer_Disable ; //DAC1输出缓存关闭 BOFF1=1
  17.     DAC_Init(DAC_Channel_2,&DAC_InitType);  //初始化DAC通道2
  18.     DAC_Cmd(DAC_Channel_2, ENABLE);  //使能DAC-CH2
  19.  
  20.     DAC_SetChannel1Data(DAC_Align_12b_R, 0);  //12位右对齐数据格式设置DAC值 
  21. }
  1. void TIM4_Int_Init(u16 arr,u16 psc)
  2. {
  3.     TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
  4.     RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); //时钟使能
  5.     TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值  计数到5000500ms
  6.     TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值  10Khz的计数频率  
  7.     TIM_TimeBaseStructure.TIM_ClockDivision = 0;     //设置时钟分割:TDTS = Tck_tim
  8.     TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
  9.     TIM_TimeBaseInit(TIM4&TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
  10.     TIM_SelectOutputTrigger(TIM4, TIM_TRGOSource_Update);//触发外设方式为更新触发
  11.  
  12.     TIM_Cmd(TIM4, ENABLE);  //使能TIMx外设
  13.         
  14. }

八、显示函数与按键控制

1.显示波形只需将所获得的1024个采样数据选择一部分进行显示大致思路如下:

  1. u16 pre_vol;//当前电压值对应点的纵坐标
  2. u16 past_vol;//前一个电压值对应点的纵坐标
  3. //adcx[]数组及通过DMA存入的1024个原始数据
  4. pre_vol = 50+adcx[x]/4096.0*100;
  5. LCD_DrawLine(x,past_vol,x+1,pre_vol);//根据实际,打点位置可进行相应更改
  6. past_vol = pre_vol;

2.按键的控制是在外部中断中进行(正点原子资料中提供相应参考代码)比较重要的是改变采样频率。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/479581
推荐阅读
相关标签
  

闽ICP备14008679号