最近在做2019年的电赛D题,其中有一个要求是‘“自动测量并显示该放大器的频幅特性曲线。显示上限频率值,相对误 差的绝对值不超过 25%。 ” 所以学习了一下简易示波器的制作
目录
硬件部分
器材:STM32F103RCT6(正点原子mini版) + 输入信号源(我采用了学校的信号发生器) 若干杜邦线 调试器(ST-link J-link 或者CMSIS-DAP都行)
原理:采用STM32F103RCT6的ADC采集功能,获取并显示输入信号的电压在TFTLCD显示屏上,根据功能需要,做出不同变化。
软件部分(注释详解)
ADC.c部分
#include "stm32f10x.h" // Device header
/*基础配置*/
#define GPIO_Pin_ADC GPIO_Pin_1
#define GPIO_Port_ADC GPIOA
#define RCC_Clock RCC_APB2Periph_GPIOA
#define ADC_Use ADC1
#define RCC_ADC RCC_APB2Periph_ADC1
/*ADC初始化*/
/**************************************
* 函 数 名: ADC_init
* 功能说明: ADC初始化
* 形 参: 无
* 返 回 值: 无
*************************************/
void ADC_init(void)
{
RCC_APB2PeriphClockCmd (RCC_Clock|RCC_APB2Periph_ADC1,ENABLE); //时钟使能
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN ; //模拟输入模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_ADC; //GPIO口配置
GPIO_Init (GPIO_Port_ADC,&GPIO_InitStructure); //GPIO配置完成
ADC_DeInit(ADC_Use);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 时钟分频
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent ; //独立模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE ; //单次转换模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //扫描模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None ;//转换由软件启动而不是外部
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right ; //右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //通道数量1
ADC_Init (ADC_Use,&ADC_InitStructure);
ADC_Cmd(ADC_Use,ENABLE ); //ADC使能
ADC_ResetCalibration(ADC_Use); //复位校准
while(ADC_GetResetCalibrationStatus(ADC_Use)); //等待复位校准结束
ADC_StartCalibration(ADC_Use);
while(ADC_GetCalibrationStatus(ADC_Use)); //等待校准结束
}
/*获取模拟量*/
/**************************************
* 函 数 名: Get_A
* 功能说明: 获取模拟量 用于转换成电压
* 形 参: 无
* 返 回 值: 模拟量
*************************************/
uint16_t Get_A(uint8_t ADC_Channel)
{
ADC_RegularChannelConfig(ADC_Use, ADC_Channel, 1, ADC_SampleTime_239Cycles5 ); //ADC_Use,ADC通道,采样时间为239.5周期
ADC_SoftwareStartConvCmd(ADC_Use, ENABLE); //使能指定的ADC_Use的软件转换启动功能
while(!ADC_GetFlagStatus(ADC_Use, ADC_FLAG_EOC ));//等待转换结束
return ADC_GetConversionValue(ADC_Use); //返回最近一次ADC_Use规则组的转换结果
}
ADC.h
#ifndef __adc_H
#define __adc_H
void ADC_init(void);
uint16_t Get_A(uint8_t ADC_Channel);
#endif
TIMER.c部分
#include "stm32f10x.h" // Device header
/*基础配置*/
#define TIM_Use TIM3 //(如果这里的TIM改了,下面的中断函数也要需要改)
#define RCC_TIM RCC_APB1Periph_TIM3
#define IRQn_TIM TIM3_IRQn
/*定时器初始化*/
/**************************************
* 函 数 名: TIM_Init
* 功能说明:定时器初始化
* 形 参: arr
* 形 参: psc
* 返 回 值: 无
*************************************/
void TIM_Init(u16 arr,u16 psc)
{
RCC_APB1PeriphClockCmd (RCC_TIM ,ENABLE); //定时器时钟使能
TIM_TimeBaseInitTypeDef TIM_InitStructure;
TIM_InitStructure.TIM_Period = arr; //计数值
TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
TIM_InitStructure.TIM_Prescaler = psc; // 预分频,此值+1为分频的除数
TIM_InitStructure .TIM_ClockDivision = 0 ; //时钟因子
TIM_TimeBaseInit (TIM_Use,&TIM_InitStructure ); // 将配置应用到定时器中
NVIC_InitTypeDef NVIC_InitStructure; //中断配置
NVIC_InitStructure .NVIC_IRQChannel = IRQn_TIM ;
NVIC_InitStructure .NVIC_IRQChannelPreemptionPriority = 0; // 设置抢占优先级
NVIC_InitStructure .NVIC_IRQChannelSubPriority = 3; // 设置相应优先级
NVIC_InitStructure .NVIC_IRQChannelCmd = ENABLE ; // 通道使能
NVIC_Init (&NVIC_InitStructure); // 应用配置
TIM_ITConfig (TIM_Use,TIM_IT_Update ,ENABLE );//允许更新中断
TIM_Cmd (TIM_Use,ENABLE); //定时器使能
}
/*定时器中断*/
/**************************************
* 函 数 名: TIM_Use_IRQHandler
* 功能说明:定时器中断
* 形 参: 无
* 返 回 值: flag_of_IRQHandler//时钟是否中断的标志flag
* 返 回 值: ms//记录数据个数(中断次数)
*************************************/
int flag_of_IRQHandler=1;//时钟是否中断的标志flag
int ms=0;//记录数据个数(中断次数)
void TIM3_IRQHandler(void) //TIM_Use中断
{
if (TIM_GetITStatus(TIM_Use, TIM_IT_Update) != RESET) //检查TIM_Use更新中断发生与否
{
TIM_ClearITPendingBit(TIM_Use, TIM_IT_Update ); //清除TIMx更新中断标志
ms++;
flag_of_IRQHandler=1;
//每次中断 ms++ 电压数据量加一
}
}
TIMER.h部分
#ifndef __TIMER_H
#define __TIMER_H
void TIM_Init(u16 arr,u16 psc);
void TIM3_IRQHandler(void);
#endif
main部分
#include "stm32f10x.h" // Device header
#include "delay.h"
#include "lcd.h"
#include "usart.h"
#include "fontupd.h"//字库检查用
#include "adc.h"
#include "TIMER.h"
#define lcd_width 240 //LCD的宽
#define lcd_height 320 //LCD的高
#define VCC_fatual 3300 //ADC基准电压 不一定都是3300mv
/*画网格线和坐标轴*/
/**************************************
* 函 数 名: net
* 功能说明: 画网格线和坐标轴
* 形 参: 无
* 返 回 值: 无
*************************************/
void net(void)
{
LCD_Clear(WHITE); //清屏
Brush_Color=GREEN; //绿色笔刷画网格线
for(int t = 5;t<lcd_height;t=t+20)//画竖着的网格
{LCD_DrawLine(0, t, lcd_height, t);}
for(int t = 5;t<lcd_height;t=t+20)//画横着的网格
{LCD_DrawLine(t,0, t,lcd_height );}
Brush_Color=RED;//红色笔刷画坐标轴
LCD_Draw_Circle(10,lcd_height-10,2);//用圆圈标注原点
//为了凸显出坐标轴 坐标轴离LCD的最大宽和最大高都差10
LCD_DrawLine(10,0,10,lcd_height );//y轴
LCD_DrawLine(0,lcd_height-10,lcd_width,lcd_height-10 );//x轴
Show_Str(230,305,12,12,"T",12,0);
Show_Str(0,0,12,12,"V",12,0);
}
//主函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
uart_init(9600); //串口初始化为9600
LCD_Init();
ADC_init();
font_init();//检查汉字库
TIM_Init(100-1,7200-1); //10Khz的计数频率(最小能测到10khz 低于10k会不准 1ms 一个点
extern int flag_of_IRQHandler,ms;//adc.h中的中断标志和数据下标
float D_arr[lcd_width];//存放D 电压数据的数组
float X_arr[lcd_width];//用于计算的数据数组
float max,min,difference; //电压量的最大值,最小值,最大最小的差值
LCD_Clear(Background_Color);//清屏
while(1)
{
if(flag_of_IRQHandler==1)//如果被中断 则记录一次数据
{
flag_of_IRQHandler = 0;//归0 等待下次中断
D_arr[ms] = Get_A(ADC_Channel_1)*VCC_fatual/4096;//模拟量换成数字量电压 以1mV为最小量度
}
if(ms>=lcd_width-10)//如果显示的个数达到要求,(建议:最大数据个数<=LDC宽度或者高度)
{
TIM_Cmd (TIM3,DISABLE);//定时器暂时关闭,先处理数据
net();//画出网格线
for(ms=0;ms<lcd_width-10;ms++)//进行最大值和最小值等处理
{
if(ms==0){max=D_arr[ms];min=D_arr[ms];}
X_arr[ms]=D_arr[ms];
if(D_arr[ms]>max){max=D_arr[ms];}
if(D_arr[ms]<min){min=D_arr[ms];}
}
difference=max-min;//最大值和最小值的差值计算
//最大值和最小值显示
Brush_Color=BLACK;//设置画笔为黑色
Show_Str(0,15,20,12,"max",12,0);
LCD_ShowNum(0,27,max,4,12);
Show_Str(0,lcd_height-30-12,20,12,"min",12,0);
LCD_ShowNum(0,lcd_height-30,min,4,12);
//显示处理 看需要等倍率放大显示,还是按照最大值为3.3v显示(二选一)
for(ms=0;ms<lcd_width-10;ms++)
{
//倍率可调 这边选了280 是调试出来的,比较适合我使用的 可以放小点或者放大点
//X_arr[ms]=D_arr[ms]/difference*280;//等倍率放大(比如3.3V能到LCD最顶部, 换成最高2V的波也能到最顶部
X_arr[ms]=D_arr[ms]/10/330*(lcd_height-10);//不按倍率放大 最顶部3.3v 最下面是0v 其余在中间
}
for(ms=0;ms<lcd_width-10;ms++)//数据显示在LCD上
{
LCD_DrawLine(ms+10,lcd_height-10-X_arr[ms],ms+11,lcd_height-10-X_arr[ms+1]);
}
ms = 0;//重新进行数据获取
TIM_Cmd (TIM3,ENABLE);//定时器重新启动
}
} }
文件需知
需要"delay.h" "lcd.h" "usart.h" "fontupd.h" "adc.h" "TIMER.h"这几个文件
其中delay.h lcd usart fontupd是采用正点原子的,请自行前往原子哥的网站下载。
delay 延时用 lcd 用于显示lcd屏 usart 用于串口通信 fontupd是汉字库文件(如果不需要显示汉字,可以不要)
adc和TIMER是这篇文章发出来的部分
实际调测图片
1.等倍率放大(在测量范围内(设定的是0~3.3v) 都能把把最高值显示在最上方,最低值呈现在最下方,等比例放大)
采用10khz的正弦波,高电平1v 低电平为0v的显示图像
采用10khz的正弦波,高电平2v 低电平为0v的显示图像
2.正常状况(最上方是3.3v 最下方0v 其他的显示在中间)
采用10khz的正弦波,高电平1.1v 低电平为0v的显示图像
采用10khz的正弦波,高电平2.2v 低电平为0v的显示图像 采用10khz的正弦波,高电平3.3v 低电平为0v的显示图像
提示(讲解):
1.在main文件里 定义了lcd_width和lcd_height 是LCD的宽高,不同尺寸的LCD需要更改
2.如果不要显示汉字的话,可以删去fontupd.h文件和font_init()等语句 就应该不会报错了
3.在main文件里 VCC_fatual是测量的基准电压 不一定都是3.3V 需要根据实际更改,不然显示会有问题的
4.在main文件里 用了TIM_Init(100-1,7200-1); 这里使用的是10khz的频率,所以本简易示波器,适用范围需要大于10khz 本人实测能到200khz左右,200k以上会失真比较严重,10k一下测不准
如果对频率有要求的,自行调整TIM_Init(100-1,7200-1); 的数值
5.adc和timer文件 只需要把上面的基础配置部分更改就可以用,注意TIMER.c中私用TIM3 如果更改了TIM_USE 记得把下面的TIM3_IRQHandler函数也更改一下
//adc.c
/*基础配置*/
#define GPIO_Pin_ADC GPIO_Pin_1
#define GPIO_Port_ADC GPIOA
#define RCC_Clock RCC_APB2Periph_GPIOA
#define ADC_Use ADC1
#define RCC_ADC RCC_APB2Periph_ADC1
//TIMER.c
/*基础配置*/
#define TIM_Use TIM3
#define RCC_TIM RCC_APB1Periph_TIM3
#define IRQn_TIM TIM3_IRQn
最重要的一点:
如果看完上面,觉得对你有帮助的,麻烦点个赞,谢谢!!!祝您生活愉快!
资源分享:
如果需要源文件,可以私信我的账号