基于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波形
正弦波形(1K)
五、代码链接
链接:https://pan.baidu.com/s/1-cHCp37KdmSaLiY3-LU4zw
提取码:bemx
来源:CSDN
作者:kuangooooo
链接:https://blog.csdn.net/weixin_45475132/article/details/104443239