STM32示波器使用详解
该项目原理图基于立创开源平台
简易数字示波器设计(入门版) – 立创开源硬件平台
一、硬件部分
以下三张分别为原理图、pcb及焊接完成图



a.前端电路
1)模拟前端(拉高电位,适配单片机电压)
在日常波形的检测当中,波形往往是各式各样的,电压幅值也有所不同。但最终是需要让单片机能够采集输入到该信号,并且在屏幕中显示出来。而常见的单片机能够接受的电压范围往往只有0-3.3V或是0-5V,不能满足稍大的电压信号以及负压。因此,需要在在前端对输入信号进行处理,其中两个重要的目标就是,降低电压峰峰值,和整体电压基准的抬升,避免负压传入单片机

该模拟前端可以简单分为一下三个部分
衰减电路
衰减电路就是通过简单的分压来降低幅值,并且通过单刀双置开关,有两档可选。
若SW5开关连接至4,相当于未经过衰减,直接采样该信号
若SW5开关连接至6,相当于电压进行分压衰减倍率为 10K/(10K+20K+20K) = 1/50
电压跟随器
中间这个电压跟随器,根据虚断可知运算放大器的输入阻抗比较大,所以运算放大器正向输入电流很小,运放输出阻抗小所以输出的电流很大,起到了很好的隔离作用
运算电路
第三部分是通过运算电路组成的信号调理电路。

先假设正向端接地,根据运放的虚短虚断,运放的反向端电压为0,可列出式子:
(Vi-0) / R13 = (0-Vo) / R15

再假设反向端接地,同样根据根据运放的虚短虚断,运放的同向端和反向端电位相同,可列出式子:
Vo\*R13/(R13+R15) = Vcc\*R8/(R4+R8)
根据两个关系式可以得出:Vo = 5/2 – Vi/2
2)比较测频电路
针对测量信号频率的方式,其中比较容易想到的就是设置一个电压比较器,在测量的周期信号,一旦高于高于该值时,进行计次,测量一段时间,取平均测得频率。但在实际的电路当中,信号并非理想的,若在设定的阈值进行小幅抖动,就会严重影响测量结果。因此,常用的方法是通过滞回电压比较器,给设定的值产生一定的阈值,在高于低阈值时计次,只有在高于高阈值时才会进行之后的计次。

滞回电压比较器
滞回电压比较器本质就是通过输出端的反馈,产生不同的阈值。其中R5和R10中间的电位大约为Vcc * 2/3,
若输出端为正Vcc,
则该比较器同向端约为(Vcc-Vcc*2/3 )* R9/(R9+R12) + Vcc*2/3
若输出端为负Vcc,
则该比较器同向端约为 [Vcc*2/3 – (-Vcc)] * R12/(R9+R12) + (-Vcc),
正是通过R12远大于R9,使得产生一个微小的阈值用于过滤。
还有几个小细节,这个电路使用的是比较器,而不是运放,比较器相比于运放,有更高的响应速度,因此更适合用于测量频率。并且,运放是通过推挽输出的,而比较器是开漏输出,因此带有一个R6的1K上拉电阻。
另外,使用了0欧姆电阻来隔离模拟地和信号地,减少对数字信号的干扰
b.电源
该设计需要多种不同的电位,例如mcu、屏幕的供电需要3.3V,运放需要+5V、-5V、3.3V
1)LDO降压电路

LDO电路的大致原理,首先是通过分压采样VIN电压,在通过运放与基准电压,放大误差,最后通过晶体管调整电流

Vout偏低 ->Va偏低 -> Vg偏低 -> Ids偏大 -> Vout
(Pmos位于可变电阻区,Vgs减小,Ids增大)
2)电荷泵负压电路
电荷泵负压电路,首先是打开开关S1和S3给中间的Cf电容充电。
在下一时刻,S1、S3断开,S2、S4闭合,通过Cf电容一方面给Co电容充电,另一方面,为输出提供负压。
并且,在下一时刻,S1、S3闭合,S2、S4断开时,Co能为输出电压提供负压
二.软件部分
整体流程图

a.采样波形-ADC+DMA
其中有两个部分与ADC的采样有关,分别是ADC的采样时钟和通道的采样速率,这与之后的屏幕显示波形有关
RCC_ADCCLKConfig(RCC_PCLK2_Div8); //ADCLK在rcc中,72M/Div
ADC_RegularChannelConfig( ADC1, ADC_Channel_3, 1, ADC_SampleTime_239Cycles5); // ADC_SampleTime_239Cycles5
如以下的三个图,均是采样了50个点,但通过了不同的采样速率和采样时钟,采样波形精度和长度就会不同。因此,软件也是通过该方式,来采集和显示不同频率下的波形。



由于只有1路ADC采样,所以选用连续转换+非扫描模式
//ADC初始化,连续转换非扫描
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //持续转换
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发方式
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //单独使用ADC
ADC_InitStructure.ADC_NbrOfChannel = 1; //通道数
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //扫描模式
ADC_Init(ADC1, &ADC_InitStructure);
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) &ADC1->DR; //ADC采样的外设地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //uint16_t
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址不自增
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) AD_Value; //DMA转移到的内存地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //uint16_t
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存的地址地址自增
DMA_InitStructure.DMA_BufferSize = 300; //缓存大小
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //DMA方向从外设传输到内存
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //关闭DMA内存到内存
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //自动循环
DMA_InitStructure.DMA_Priority = DMA_Priority_High; //开启高优先级
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
b.TFT屏幕-SPI
屏幕的移植部分略,重点说明画曲线的函数。
画出的曲线本质就是很多条直线连接起来,因此,只需要在x轴上,随时间向右增加,根据采集到的电压确定y轴。确定了x,y的坐标,再将该点的与上一点使用直线相连即可。
再是确定屏幕显示到的位置,可以分为三个部分–初始点,超出范围和其他部分,分别配置不同的逻辑即可。
void drawCurve(uint8_t yOffset,short int rawValue)
{
uint16_t i=0,j=0;
uint16_t x=0,y=0;
y = yOffset - rawValue; //data processing code
if(firstPoint)//如果是第一次画点,则无需连线,直接描点即可
{
TFT_DrawPoint(0,y,GREEN);
lastX=0;
lastY=y;
firstPoint=0;
}
else
{
x=lastX+1;
if(x<100) //不超过屏幕宽度
{
TFT_DrawLine(lastX,lastY,x,y,GREEN);
for(i=1;i<90;i++) //对将要显示的下一个点清屏,范围为y:40->90
{
TFT_DrawPoint(x+1,i,BLACK);
}
lastX=x;
lastY=y;
}
else //超出屏幕宽度,清屏,从第一个点开始绘制,实现动态更新效果
{
//TFT_Fill(0,0,160,128,WHITE);//清屏,白色背景
TFT_DrawPoint(0,y,GREEN);
lastX=0;
lastY=y;
}
}
}
c.采样频率-输入捕获
输入捕获初始化部分配置引脚、IC通道、定时中断源、NVIC中断管理,代码略
频率测量使用的是测周法:即两个上升沿内,以标准频率fc计次,得到N ,则频率fx=fc / N

(图片取自江科大的ppt)
void TIM3_IRQHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET)
{
// 获取捕获值
IC1Value = TIM_GetCapture1(TIM3);
if (IC1Value != 0)
{
// 计算频率
fre = 1000000 / IC1Value;
NoInputFlag = 0;
UpdateCounter = 0;
}
// 清除捕获中断标志位
TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);
}
}
d.方波产生-输出比较

重点分别配置OC的模式和极性,其他略
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;

e.旋转编码器-外部中断
使用外部中断,不需要主控每次都判断一次,提高了工作效率。同时,若使用定时器的编码器模式,会消耗掉一个定时器,有点耗费资源了

先配置外部中断为下降沿触发,当外部中断触发后,在判断另一引脚的电位。(以下代码为反向旋转示例)
// 反向旋转
EXTI_InitTypeDef EXIT_InitStructure;
EXIT_InitStructure.EXTI_Line = EXTI_Line3 | EXTI_Line4;
EXIT_InitStructure.EXTI_LineCmd = ENABLE;
EXIT_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXIT_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXIT_InitStructure);
void EXTI3_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line3) == SET)
{
Delay_ms(10);
if (GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_4) == 0)
{
adc_div++;
if (adc_div>5)
{adc_div = 5;}
}
EXTI_ClearITPendingBit(EXTI_Line3);
}
}
f.主程序处理
PWM输出控制
if (pwm_flag)
{
TFT_ShowString(110,36," open ",BLACK,YELLOW,16,0);
TIM_Cmd(TIM2, ENABLE);
// 占空比
if (duty_flag == 0)
{
TFT_ShowString(110,105," 20% ",BLACK,YELLOW,16,0);
PWM_SetCompare(20);
}
else if (duty_flag == 1)
{
TFT_ShowString(110,105," 50% ",BLACK,YELLOW,16,0);
PWM_SetCompare(50);
}
else if (duty_flag == 2)
{
TFT_ShowString(110,105," 80% ",BLACK,YELLOW,16,0);
PWM_SetCompare(80);
}
// 频率/Hz
if (fre_flag == 0)
{
TFT_ShowString(110,72," 1K ",BLACK,YELLOW,16,0);
PWM_SetPrescaler(720-1);
}
else if (fre_flag == 1)
{
TFT_ShowString(110,72," 2K ",BLACK,YELLOW,16,0);
PWM_SetPrescaler(360-1);
}
else if (fre_flag == 2)
{
TFT_ShowString(110,72," 4K ",BLACK,YELLOW,16,0);
PWM_SetPrescaler(180-1);
}
}
else
{
TFT_ShowString(110,36,"close ",BLACK,YELLOW,16,0);
TFT_ShowString(110,72," ",BLACK,YELLOW,16,0); // f/Hz
TFT_ShowString(110,105," ",BLACK,YELLOW,16,0); // 占空比
TIM_Cmd(TIM2, DISABLE);
}
频率采集及显示
将数据的每一位以数组的形式存在now_fre[]里,并在最后显示
if (NoInputFlag) //在输入捕获中断添加的标志位,使得无输入时显示归零
{
TFT_ShowString(55,105," 0.0K", BLACK,GREEN,16,0);
TFT_ShowString(5,105," 0.0V ",BLACK,GREEN,16,0);
}
else
{
// 避免小抖动干扰
if(VppMean <= 0.3)
{
fre=0;
}
if (fre < 10000)
{
now_fre[0] = ' ';
now_fre[1] = fre/1000 + '0';
now_fre[3] = fre%1000/100 + '0';
TFT_ShowString(55,105, now_fre, BLACK,GREEN,16,0);
}
else if (fre < 100000)
{
now_fre[0] = fre/10000 +'0';
now_fre[1] = fre%10000/1000 + '0';
now_fre[3] = fre%1000/100 + '0';
TFT_ShowString(55,105, now_fre, BLACK,GREEN,16,0);
}
}
ADC采样速率调节
switch(adc_div)
{
case 1:
RCC_ADCCLKConfig(RCC_PCLK2_Div8);
ADC_RegularChannelConfig( ADC1, ADC_Channel_3, 1, ADC_SampleTime_239Cycles5);
break;
case 2:
RCC_ADCCLKConfig(RCC_PCLK2_Div4);
ADC_RegularChannelConfig( ADC1, ADC_Channel_3, 1, ADC_SampleTime_239Cycles5);
break;
case 3:
RCC_ADCCLKConfig(RCC_PCLK2_Div2);
ADC_RegularChannelConfig( ADC1, ADC_Channel_3, 1, ADC_SampleTime_239Cycles5);
break;
case 4:
RCC_ADCCLKConfig(RCC_PCLK2_Div2);
ADC_RegularChannelConfig( ADC1, ADC_Channel_3, 1, ADC_SampleTime_71Cycles5);
break;
case 5:
RCC_ADCCLKConfig(RCC_PCLK2_Div2);
ADC_RegularChannelConfig( ADC1, ADC_Channel_3, 1, ADC_SampleTime_28Cycles5);
break;
}
电压幅值测量及显示
使用遍历的方式取出这一组中电位最高的值(300个采样点),并且将这些采样点存在voltage[]数组中,在之后的波形显示中使用
Vpp=0;
for(i=0;i<300;i++)
{
tempVoltage[i] = (float)AD_Value[i]*3.3f/4095.0f; //单片机测得的实际电压
voltage[i] = 5-(2.0f*tempVoltage[i]); //为前端电路处理前的电压
if (Vpp < voltage[i]) //取出电压最大值
{
Vpp = voltage[i];
}
if (min > voltage[i])
{
min = voltage[i];
}
}
// 电压输入幅值抖动严重,均值滤波
VppSum += Vpp;
j++;
if (j%5 == 0)
{
VppMean = VppSum/5;
VppSum=0;
j=0;
}
now_vpp[1] = (uint8_t)(VppMean)%10 + '0';
now_vpp[3] = (uint8_t)(VppMean*10)%10 + '0';
TFT_ShowString(5,105, now_vpp, BLACK,GREEN,16,0);
波形初始点选取,滤去小噪声
for(i=0;i<200;i++)
{
if(voltage[i] < max_data)
{
for(;i<200;i++)
{
if(voltage[i] > max_data)
{
Trigger_number=i;
break;
}
}
break;
}
}
计算显示的波形与实际电压范围的放大
if(VppMean > 0.3)
{
if(min < -0.3)
{
median = VppMean;
}
else
{
median = VppMean / 2.0f;
}
//放大倍数,需要确定放大之后的区间,我将波形固定显示在(18.75~41.25中),(41.25-18.75)/2=11.25f
gainFactor = 11.25f/median; // 显示范围宽度/波形的整个高度 = 放大倍数
}
波形显示
for(i=Trigger_number;i<Trigger_number+100;i++)
{
if(min < -0.3)
{
voltage_output = voltage[i] + VppMean;
}
else
{
voltage_output = voltage[i];
}
voltage_output = voltage[i];
if(voltage_output >= median)
{
voltage_output = 30 + (voltage_output - median)*gainFactor;
}
else
{
voltage_output = 30 - (median - voltage_output)*gainFactor;
}
drawCurve(80,voltage_output);
}
作者:空nk.

