基于stm32mini开发板的简易函数发生器和简易示波器

五迷三道 提交于 2020-02-27 11:00:48

基于stm32 mini开发板的简易函数发生器和简易示波器

前言:这是我学习完stm32基础知识后做的第一个比较综合的项目,由于本人学习时间不长,在程序设计方面能力不强,故展示的代码或者方法可能有误,还请各位大佬海涵,我也很高兴大家能在评论区提出建议和意见,谢谢。

一、项目整体思路和实现的功能
这个项目是基于正点原子stm32 mini开发板设计的,使用芯片为STM32F103RCT6,相关配置步骤和基础知识,可以在正点原子论坛找到。
(一)、简易示波器思路和功能
利用stm32强大的ADC功能,在一定时间内采集IO口电压,将采集到的一定数值保存在数组中,经过数据处理后,显示在LCD上。
能实现正电压下,0~3.3v电压的显示,以及最高10KHZ的频率显示(10K以上显示将不清晰)。能通过两个按键实现对ADC采样周期的转换,分为us级和ms级。
(二)、简易函数发生器思路和功能
利用stm32强大的DAC和DMA功能,以定时器2触发DAC转换,以DMA传送需要转换的数值,以达到目标波形的输出。
能实现正弦波,三角波,方波,锯齿波,甚至模拟噪声波等多种波形的输出,可以调节输出波形的幅值和频率。

二、程序设计和部分原理解释
(一)、外围按键设计
这部分主要涉及改变ADC采样周期,由于整个程序有延时,必须采用中断的方式读取键值并改变采样周期标志位,这样才能达到按一次改变一次的效果。但是任然存在延时,按了按键以后需要等到下一个循环周期才能改变LCD显示。
这部分包括按键初始化,键值读取,外部中断配置。按键初始化和初始化GPIO口大同小异,此处不再赘述。主要展示外部中断配置。

void EXTIX_Init(void)
{
  EXTI_InitTypeDef EXTI_InitStructure;
  NVIC_InitTypeDef NVIC_InitStructure;
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//打开AFIO时钟
  
  KEY_INT();                        //初始化按键
  //配置外部中断
  GPIO_EXTILineConfig(GPIO_PortSourceGPIOC,GPIO_PinSource5);//key0 PC5引脚
  EXTI_InitStructure.EXTI_Line=EXTI_Line5;
  EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; 
  EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;   
  EXTI_InitStructure.EXTI_LineCmd = ENABLE;
  EXTI_Init(&EXTI_InitStructure);  
  
  GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource15);//key1 PA15引脚
  
  EXTI_InitStructure.EXTI_Line=EXTI_Line15;
  EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; 
  EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;    
  EXTI_InitStructure.EXTI_LineCmd = ENABLE;
  EXTI_Init(&EXTI_InitStructure);    
  
  GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);//wk_up PA0引脚
  EXTI_InitStructure.EXTI_Line=EXTI_Line0;
  EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; 
  EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;    
  EXTI_InitStructure.EXTI_LineCmd = ENABLE;
  EXTI_Init(&EXTI_InitStructure); 
  
  NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;   
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;//抢占优先级2
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;     //子优先级1
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;       
  NVIC_Init(&NVIC_InitStructure);
  NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;   
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;     
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;        
  NVIC_Init( &NVIC_InitStructure);
  NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;   
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; 
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;    
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;       
  NVIC_Init( &NVIC_InitStructure); 
}

对于在按键的中断函数中如何实现让采样周期变长或缩短,我这里是通过改变标志位,在主函数中判断标志位的值来改变的。一是可以改变时间单位,即ms或者us,二是可以改变数值,我设置了六个数值,分别为20,40,60,80,100,120。大家也可以根据自己的需要更改。这里不再赘述。

(二)、ADC初始化和数值获取
初始化ADC1的PC0口输入。12M时钟,最小采样周期为7us,为达到比较好的效果,我人为设置成最小20us。

void adc_init(void)
{
 GPIO_InitTypeDef GPIO_InitStruct;
 ADC_InitTypeDef ADC_InitStruct;
 //开启ADC1和相应IO口时钟
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_ADC1,ENABLE);  
 //ADC时钟由主时钟六分频
 RCC_ADCCLKConfig(RCC_PCLK2_Div6);      
 //PC0初始化
 GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AIN;
 GPIO_InitStruct.GPIO_Pin=GPIO_Pin_0;
 GPIO_Init(GPIOC, &GPIO_InitStruct);     
 
 ADC_DeInit(ADC1);
 //ADC1初始化
 ADC_InitStruct.ADC_ContinuousConvMode=DISABLE;
 ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;
 ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
 ADC_InitStruct.ADC_Mode=ADC_Mode_Independent;
 ADC_InitStruct.ADC_NbrOfChannel=1;    
 ADC_InitStruct.ADC_ScanConvMode=DISABLE;
 ADC_Init(ADC1,&ADC_InitStruct);        
 //使能ADC1
 ADC_Cmd(ADC1,ENABLE);             
 //使能复位校准
 ADC_ResetCalibration(ADC1);       
 //等待复位校准结束
 while(ADC_GetResetCalibrationStatus(ADC1))//使能ADC校准
 ADC_StartCalibration(ADC1);
 //等待校准完成               
 while(ADC_GetCalibrationStatus(ADC1));      
}

接下来是ADC值获取,换取之后将其转换成电压值,需要强调的是,转换时间遵循以下,T转换=采样时间(可以设置的ADC时钟周期)+12.5个ADC时钟周期,为了方便主函数处理,电压值转换成以3300为峰值的四位数。

u16 adc_get(void)
{
 u16 value=0;
 //转换周期为7us 
 ADC_RegularChannelConfig(ADC1,ADC_Channel_10,1,ADC_SampleTime_71Cycles5);
 ADC_SoftwareStartConvCmd(ADC1,ENABLE);
 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC )); 
 value=ADC_GetConversionValue(ADC1);   
 value=(int)value*3.3*1000/4096; 
 return value;
}

(三)、ADC数据处理部分
这一步是将ADC采集的值转换成便于LCD显示的值后存储

while(i<160)
  {
   value[i]=adc_get();
   //由于返回的ADC的值是四位整数,且显示电压部分像素点共120个,对应每个点
   //0.0275v,故将采集到的电压值除以27以便求出每个电压对应的像素点个数
   value[i]=(int)value[i]/27;   
   if(us_ms==1) delay_ms(time_get);
   else delay_us(time_get-16);
   i++;
  }
  i=0;

(四)、LCD显示设置
LCD使用的是正点原子的2.4*2.8的屏幕,所以直接采用正点原子提供的库函数。对于LCD的初始化和相关函数使用,可以参考正点原子相关文档。这里展示如何在LCD上描绘波形。我采用的是采集160个ADC值,由于是横屏显示,有320个像素点,所以每隔一个点描绘一个ADC值,连线后就是波形啦。LCD_DrawLine(X1,Y1,X2,Y2)函数中的参数为:X1为X轴起点,X2为X轴终点,Y1为Y轴起点,Y2为Y轴终点。

while(i<159)
  {
   POINT_COLOR=RED;
   LCD_DrawLine(i*2,120-value[i],(i+1)*2,120-value[i+1]);
   delay_ms(5);
   i++;
  }
  i=0;

(五)、DAC初始化
这里DAC需要用到定时器触发,并且开启DMA使能。

void dac_init(void) 
{
 GPIO_InitTypeDef GPIO_InitStruct;
 DAC_InitTypeDef DAC_InitStruct;
 
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
 RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC,ENABLE);
 //GPIO_Mode也可以设置成模拟输入
 GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;
 GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;
 GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
 GPIO_Init(GPIOA, &GPIO_InitStruct);
 //结构体成员初始化一定要有,不然会出错。
 DAC_StructInit(&DAC_InitStruct);
                       
 DAC_InitStruct.DAC_OutputBuffer=DAC_OutputBuffer_Disable;
 DAC_InitStruct.DAC_Trigger=DAC_Trigger_T2_TRGO;//定时器2触发
 DAC_InitStruct.DAC_WaveGeneration=DAC_WaveGeneration_None;
 DAC_Init(DAC_Channel_1, &DAC_InitStruct);
 
 DAC_Cmd(DAC_Channel_1,ENABLE);
 //开启DMA
 DAC_DMACmd(DAC_Channel_1,ENABLE);
}

(六)、定时器初始化
此函数需要传入波形输出频率,由于在定时器初始化函数中赋值给TIM_TimeBaseInitStruct.TIM_Period成员的实际上是定时器重装载值,所以需要将传入的参数进行转换再赋给这个成员。具体转换见程序:

void timer_init(u32 f)   
{
 TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
 //将传入的参数转换成定时器重装载值
 f=(u16)(72000000*2/sizeof(dac_out)/f);            
 
 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
 
 TIM_TimeBaseStructInit(&TIM_TimeBaseInitStruct);   
 TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;//不预分频  72M
 TIM_TimeBaseInitStruct.TIM_Period=f;             
 TIM_TimeBaseInitStruct.TIM_Prescaler=0x00; //不时钟分割
 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);
 //更新事件触发
 TIM_SelectOutputTrigger(TIM2,TIM_TRGOSource_Update);
}

(七)、DMA初始化
DMA主要功能是将存储在内存或外设的数据,不经过CPU直接传送给目标寄存器或者外设,能节省CPU分配,也能加快程序运行效率。这里是将计算好的DAC值直接传送给DAC寄存器。DMA采取内存递增,循环模式。当TIM2产生更新事件时,DAC将最近存放在寄存器DAC_DHRX中的数据传送至寄存器DAC_DORX中,从而产生电压,同时DAC使能了DMA,当产生一个电压后就会触发DMA,从而得到下一个电压值。对于DMA循环功能,如果下一个内存超出指定最大位置时就会回到开始位置。关于外设地址,可以在stm32f10x.h文件中查找。内存地址就是存放电压值的数组名。

void dma_init(void)
{
 DMA_InitTypeDef DMA_InitStruct;
 //开启时钟
 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2,ENABLE);
 //初始化结构体成员
 DMA_StructInit( &DMA_InitStruct);     
 DMA_InitStruct.DMA_BufferSize=much;   //much在主函数中定义 是数组成员个数
 DMA_InitStruct.DMA_DIR=DMA_DIR_PeripheralDST;  //由内存到外设
 DMA_InitStruct.DMA_M2M=DMA_M2M_Disable;       //内存到内存关闭
 DMA_InitStruct.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;
 DMA_InitStruct.DMA_MemoryInc=DMA_MemoryInc_Enable; //内存地址递增
 DMA_InitStruct.DMA_Mode=DMA_Mode_Circular;//循环模式
 DMA_InitStruct.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;
 DMA_InitStruct.DMA_PeripheralInc=DMA_PeripheralInc_Disable;//外设地址不递增
 DMA_InitStruct.DMA_Priority=DMA_Priority_VeryHigh;//等级非常高
 
 DMA_InitStruct.DMA_MemoryBaseAddr=(uint32_t)dac_out;//内存地址
 DMA_InitStruct.DMA_PeripheralBaseAddr=DAC_DHR12R2; //DAC地址  在主函数中定义
 
 DMA_Init(DMA2_Channel3, &DMA_InitStruct);
 
 DMA_Cmd(DMA2_Channel3,ENABLE);
}

(八)、波形表数值的产生
本来是将波形产生的函数设计在MDK程序中的,可是在实际运行中波形输出不好,估计是受限于32对浮点数的计算能力,所以我在VC6.0中设计了一个程序,以计算输出不同波形下不同样点个数的波形数值表。由于简易示波器无法显示负电压,所以需要有个基础电压,这里我设置成1.6v,当然如果想要修改对应峰值,改1.6就行。这点很容易理解。各个波形的32——256位的波形表我在文件中都有分享,下面是程序:

#include<stdio.h>
#include<math.h>
#define much 32  //波形数组里的成员个数
#define much_float 32.0000 //便于计算,分母必须是浮点型  输出才准确
int value[much]int i;
 for(i=0;i<much;i++)
 {  
      //锯齿波产生函数
      if(i<=(much/4-1))  value[i]=(1.6+1.6/(much_float/4)*i)*4095/3.3;
      if(i>(much/4-1)&&i<=(much/4-1+much/2))   
      value[i]=(3.2/(much_float/2)*(i-(much/4)))*4095/3.3;
      if(i>(much/4-1+much/2)&&i<much)  
      value[i]=(0+1.6/(much_float/4)*(i-(much/4-1+much/2)))*4095/3.3;

      //三角波函数
    /*if(i<=(much/4-1))  value[i]=(1.6+1.6/(much_float/4)*i)*4095/3.3;
      if(i>(much/4-1)&&i<=(much/4-1+much/2))   
      value[i]=(3.2-3.2/(much_float/2)*(i-(much/4)))*4095/3.3;
      if(i>(much/4-1+much/2)&&i<much)   
      value[i]=(0+1.6/(much_float/4)*(i-(much/4-1+much/2)))*4095/3.3;*/

      ////正弦波函数
      //value[i]=((1.6*sin(i/32.00*2*3.14)+1.6)*4095/3.3);   
 for(i=0;i<much;i++)
 {
  printf("%d,",value[i]);
 }
}

三、注意事项
(一)、关于示波器部分
1、LCD显示的总时间是:160*Tadc转换时间,所以波形周期可以根据查看显示的波形周期数和LCD显示的时间得到。如果想要改采集的点数,修改相应的数组大小和LCD显示程序即可。
2、对转换时间误差的个人理解(大佬勿喷):由于单片机执行程序时会耗时,ADC转换时间又是us级别,以20us为实际转换周期,除去初始化时设置的7us的转换时间,本来需要延时13us,由于 程序执行时间的消耗,实际延时不能是13us,这样会使转换周期与设计有较大偏差,经过测试,延时时间应是(20-16)us,如果需要设置其他转换周期,将20改掉即可。
3、由于ADC最大读取值是3.3v,当采集的峰值大于该值时,为了方便,我直接采用分压电路,将信号分压后再采入。
(二)、关于函数发生器部分
1、注意函数发生器的频率不能超过20k。
2、当波形表数组大小越大时所能展现的波形越细致。

四、效果展示

PWM波形pwm波形
正弦波形(1K)
在这里插入图片描述
五、代码链接
链接:https://pan.baidu.com/s/1-cHCp37KdmSaLiY3-LU4zw
提取码:bemx

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!