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

前言:用正点原子的mini开发板,设计制作简易示波器和简易函数发生器,需要运用的知识是 ADC+DAC+DMA+通用定时器+外部中断。

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

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

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;//抢占优先级2NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;     //子优先级1NVIC_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口作为ADC的输入端口。采用6分频,即12M时钟作为ADC的时钟,选取ADC最小采用时间是71.5个时钟周期,外加12.5个固定的转换时钟周期,如此计算可得ADC最小采样周期为1/72M*90=7.5us,为达到比较好的效果,人为设置成最小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);        //使能ADC1ADC_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描点和连线的函数可以采用正点原子官方函数,例如连线的函数是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初始化
使用的Mini开发板的RCT6芯片有两个DAC,即DAC通道1,对应PA4口, DAC通道2,对应PA5口。需要注意的是DAC自带的输出缓存功能,如果使能该功能,虽然带负载能力更强,但是输出无法到0。同时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);//开启DMADAC_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;//不预分频  72MTIM_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为实际转换周期,除去初始化时设置的7.5us的转换时间,本来需要延时12.5us,由于 程序执行时间的消耗,实际延时不能是12.5us,这样会使转换周期与设计有较大偏差,经过测试,延时时间应是(20-16)us,如果需要设置其他转换周期,将20改掉即可。
3、由于ADC最大读取值是3.3v,当采集的峰值大于该值时,为了方便,我直接采用分压电路,将信号分压后再采入。
4、配置DAC功能时,GPIO引脚需要设置为模拟输入,为了避免寄生的干扰和额外的功耗。
5、在配置DMA功能时,需要极其注意外设地址是否正确。查找外设的基地址在头文件stm32f10x.h内,例如需要查找DAC的外设基地址,找到DAC,进入DAC的结构体,就可以查找到了:


(二)、关于函数发生器部分
1、注意函数发生器的频率不能超过20k。
2、当波形表数组大小越大时所能展现的波形越细致。

四、效果展示

PWM波形
正弦波形(1K)

五、代码链接
链接:https://pan.baidu.com/s/1uj0lsuUQDT52blApl6ZZkQ
提取码:19a1

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

  1. 移植根文件系统到linux内核 s3c2440,u-boot-2011.06在基于s3c2440开发板的移植之引导内核与加载根文件系统...

    三.根文件系统的制作 我们利用busybox来制作根文件系统 1.在下列网站下载busybox-1.15.0.tar.bz2 在当前目录下解压busybox tar -jxvf busybox-1.1 ...

  2. 【RTOS】基于V7开发板的uCOS-III,uCOS-II,RTX4,RTX5,FreeRTOS原版和带CMSIS-RTOS V2封装层版全部集齐...

    RTOS模板制作好后,后面堆各种中间件就方便了. 1.基于V7开发板的最新版uCOS-II V2.92.16程序模板,含MDK和IAR,支持uC/Probe https://www.cnblogs.c ...

  3. 基于uFUN开发板的心率计(三)Qt上位机的实现

    前言 上两周利用周末的时间,分别写了基于uFUN开发板的心率计(一)DMA方式获取传感器数据和基于uFUN开发板的心率计(二)动态阈值算法获取心率值,介绍了AD采集传感器数据和数据的滤波处理获取心率值 ...

  4. 基于uFUN开发板的RGB调色板

    前言 使用uFUN开发板配合Qt上位机,实现任意颜色的混合,Qt上位机下发RGB数值,范围0-255,uFUN开发板进行解析,然后输出不同占空比的PWM,从而实现通过RGB三原色调制出任意颜色. Qt ...

  5. request[limit]取不到前台的值_基于uFUN开发板的心率计(二)动态阈值算法获取心率值...

    前言 上一篇文章:基于uFUN开发板的心率计(一)DMA方式获取传感器数据,介绍了如何获取PulseSensor心率传感器的电压值,并对硬件电路进行了计算分析.心率计,重要的是要获取到心率值,本篇文章 ...

  6. 基于uFUN开发板和扩展板的联网校准时钟

    项目概述 上周在uFUN试用群里看到管理员说试用活动快结束了,要抓紧完成评测总结,看大家的评测总结也都写了,我也不能落后啊!正好最近做的扩展板到手了,于是赶紧进行调试,做了一个不用校准的时钟,时钟这种 ...

  7. 基于uFUN开发板的心率计(二)动态阈值算法获取心率值

    文章目录 前言 IBI和BPM 核心操作 -- 识别一个脉搏信号 问题一:阈值的选取 问题二:特征点识别 算法整体框架与代码实现 总结 基于uFUN开发板的Keil源码下载 uFUN评测系列文章 前言 ...

  8. 基于STM32开发板I²C总线通信协议浅析

    基于STM32开发板I²C总线通信协议浅析 一.前言 I²C(Inter-Integrated Circuit),中文应该叫集成电路总线,它是一种串行通信总线,使用多主从架构,是由飞利浦公司在1980 ...

  9. 【基于NSR3588开发板Android12三屏拼接显示实例】

    基于NSR3588开发板Android12三屏拼接显示实例 1.硬件接口 2路HDMI接口 1路Type-c DP转HDMI 如下图: 2.多屏显示拼接简介 RK3588可以支持如下多种拼接模式: 我 ...

最新文章

  1. 爱奇艺大数据分析平台的演进之路
  2. linux 磁盘挂载sde,linux lvm挂载新的硬盘并且扩容
  3. sizeof()计算结构体的大小
  4. 交易所行情报盘程序配置
  5. 面向对象思想设计原则
  6. dom文档对象模型图
  7. 安装java项目开发环境
  8. Hadoop学习目录导航
  9. 能测试快充真假的软件,苹果iOS 12可自行测试真假快充:山寨充电器无处遁形
  10. 摩伴windows桌面服务器,魔伴windows桌面
  11. 优盘复制进来为空_U盘问题 复制文件夹到U盘后,再打开就成空的了、
  12. CN基于词库的中文转拼音优质解决方案,单类单文件版,支持低版本PHP
  13. 2022-2028年全球与中国近红外照相机行业发展趋势及投资战略分析
  14. C#Task执行线程及其相关问题
  15. 微博html5到桌面,HTML 分享页面到QQ/微信、微博等平台
  16. postma公共变量的设置及使用
  17. 关于基金的各种名词含义及来源,小白如何入门基金
  18. STM32F103CubeMX定时器
  19. 最优化理论——信赖域方法
  20. CentOS 8 OpenSSL 问题表述与解决

热门文章

  1. 清空GitHub仓库的历史提交记录(commits)
  2. ubuntu桌面特效
  3. 时装过滤效果Lr预设
  4. 要想学好C语言,你必须得懂的基础知识大全!本文将带你深度学习
  5. VS2015配置OpenGL环境——GLUT、freeglut、glew、GLtools
  6. python如何循环执行_python循环执行语句怎么写
  7. 电脑怎么卸载软件?彻底卸载软件,4个方法分享
  8. kdb 使用手册指导 1
  9. 灵魂拷问:TCP 四次挥手,可以变成三次吗?
  10. 为什么要在项目中使用缓存呢?