基于STM32G031的失真度测试仪(CubeMX+ADC+DMA+OLED+EC11)

news/2024/12/28 20:05:16/

目录

    • 项目介绍
    • 硬件介绍
    • 设计思路
    • 各功能代码及说明
      • SPWM波生成
      • ADC采样
      • FFT
      • 获取按键动作
      • OLED显示
      • 系统顶层
    • 功能展示
      • OLED显示采样波形
      • OLED显示频谱/失真度曲线
    • 项目总结

👉工程文件及代码:参见【2022寒假在家练】基于STM32G031的失真度测试仪
👉 CSDN:工程源代码下载
👉 Github-KafCoppelia/DistortionMeter

项目介绍

本项目基于电子森林的STM32G031口袋仪器训练平台,基于CubeMX与Keil,实现了:

  1. 通过芯片的PWM+板上LPF电路生成频率在DC~20KHz,频率可调,并且幅度可调,从10mV~500mV正弦波信号;
  2. 将该信号通过Test端口连接到测试电路的输入端,通过运算放大器输入至ADC+DMA,对其进行量化处理;
  3. 计算该电路频谱(归一化幅值谱),与总谐波(THD);
  4. 在OLED上绘制了波形图及归一化幅值谱、失真度曲线(线性及对数坐标)。

硬件介绍

👉 电子森林-基于STM32的简易示波器/频谱仪/信号发生器学习平台

  1. 基于STM32G031微控制器,Arm Cortex M0+内核,主频为64MHz;
  2. 2个按键+1个光电旋转编码器用于控制输入;
  3. 1个SPI接口的OLED显示屏(128*128分辨率);
  4. 1路音频放大电路用于产生ADC的测试信号,并可作为测试电路使用;
    一个蜂鸣器用于音效输出;
  5. 1路基于PWM的DDS信号输出,用于产生测试信号(任意波形);
  6. 2路增益可调的模拟信号输入,通过12bits ADC采集2mVpp~30Vpp,带宽为100KHz的模拟信号;

基于STM32G031的测试测量学习套件的构成框图
基于STM32G031的测试测量学习套件的构成框图如上图所示。

设计思路

设计的整体结构框图如下图所示:

👉 整体结构参考:SCOPE-F072–基于STM32F072的多功能掌中仪器

整体结构框图
由于没有上操作系统,初始化外设及OLED后,整体为一个循环,判断当前系统处于示波器或频谱/失真度状态(即OLED显示的内容是什么),再各判断是否为页初始化(初次进入该状态时会设置状态,之后相互切换就不会重置状态位),之后执行对应的操作。按键、旋钮的交互功能如上图箭头所示。

各功能代码及说明

该开发板电路图如图所示:

电路图
板卡左下角PB0产生PWM,可通过示波器测PWM测试点调试,连接左上角JP1的1、2(实物排针右两个),通过对ADC_M(PA0)做采样即可获取波形数据。

SPWM波生成

PWM信号源相关代码参见source.c/.h

SPWM波主要是调节一般PWM波的占空比,使输出波所占面积和对应正弦波面积相等。所以首先需要一组正弦波数据,可以通过Python等方式计算:

import numpy as npdef sin_wave(point, num):y = []for i in range(0, point):fz = num/2 * np.sin(np.pi/point*2*i) + num/2y.append(fz)return yif __name__ == "__main__":y = sin_wave(256, 256)print(y)y2 = []for dot in y:y2.append(round(dot))print(y2)

其中,point为生成数据的点数,如128、256个;num为生成数据的范围,表示从0~num。生成的数据可以static const uint16_t储存:

#define SIGNAL_LENGTH 256
static const uint16_t sine_table[SIGNAL_LENGTH] = {128, 131, 134, 137, 141, 144, 147, 150, 153, 156, 159, 162, 165, 168, 171, 174, 177, 180, 183, 186, 188, 191, 194, 196, 199, 202, 204, 207, 209, 212, 214, 216, 219, 221, 223, 225, 227, 229, 231, 233, 234, 236, 238, 239, 241, 242, 244, 245, 246, 247, 249, 250, 250, 251, 252, 253, 254, 254, 255, 255, 255, 256, 256, 256,256, 256, 256, 256, 255, 255, 255, 254, 254, 253, 252, 251, 250, 250, 249, 247, 246, 245, 244, 242, 241, 239, 238, 236, 234, 233, 231, 229, 227, 225, 223, 221, 219, 216, 214, 212, 209, 207, 204, 202, 199, 196, 194, 191, 188, 186, 183, 180, 177, 174, 171, 168, 165, 162, 159, 156, 153, 150, 147, 144, 141, 137, 134, 131, 128, 125, 122, 119, 115, 112, 109, 106, 103, 100, 97, 94, 91, 88, 85, 82, 79, 76, 73, 70, 68, 65, 62, 60, 57, 54, 52, 49, 47, 44, 42, 40, 37, 35, 33, 31, 29, 27, 25, 23, 22, 20, 18, 17, 15, 14, 12, 11, 10, 9, 7, 6, 6, 5, 4, 3, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 4, 5, 6, 6, 7, 9, 10, 11, 12, 14, 15, 17, 18, 20, 22, 23, 25, 27, 29, 31, 33, 35, 37, 40, 42, 44, 47, 49, 52, 54, 57, 60, 62, 65, 68, 70, 73, 76, 79, 82, 85, 88, 91, 94, 97, 100, 103, 106, 109, 112, 115, 119, 122, 125
};

将TIM3_CH3(PB0)设为PWM输出,prescalercounter period暂且不管。从正弦波表至设置频率的SPWM波及振幅还需变换:

void Generate_Sine(void)
{uint16_t tim_period;uint16_t i;if(is_source_on())Sine_Stop();tim_period = 64000000 / SIGNAL_LENGTH / source_signal.frequency; // 25__HAL_TIM_SET_AUTORELOAD(&htim3, tim_period-1);for (i = 0; i < SIGNAL_LENGTH; i++)sine_value[i] = (sine_table[i]-128) * (tim_period-1) / 256 * source_signal.amplitude / 1650 + tim_period/2;if(!is_source_on())Sine_Start();
}

根据:
f S P W M = f s i n e ⋅ N f_{SPWM}=f_{sine}\cdot N fSPWM=fsineN

由此算得TIM3的周期,通过__HAL_TIM_SET_AUTORELOAD()设定不同频率正弦波下的SPWM的频率。此外,还需对正弦波表做归一化,将其直流偏置移动到1.65V(IO口输出最高3.3V),其中source_signal为信号源参数的结构体,储存源信号的频率和幅度,开始/关闭产生正弦波调用
HAL_TIM_PWM_Start_DMA()HAL_TIM_PWM_Stop_DMA()即可。

typedef struct
{uint32_t frequency;uint16_t amplitude;
} Source_Params;
/*-----------------*/
Source_Params source_signal = {.frequency = 1000, .amplitude = 500};void Sine_Start(void)
{   HAL_TIM_PWM_Start_DMA(&htim3, TIM_CHANNEL_3, (uint32_t*)sine_value, SIGNAL_LENGTH);set_source_on();
}void Sine_Stop(void)
{HAL_TIM_PWM_Stop_DMA(&htim3, TIM_CHANNEL_3);set_source_off();
}

由于当设置好输出频率及振幅后,传输至TIM3_CH3的AutoReload值为周期性循环的,因此设置TIM3_CH3的DMA有利于提高CPU的效率:

ParametersValue
Channel随意
DirectionMemory to Peripheral
PriorityMedium
ModeCircular
Increment AddressMemory
Data Width of PeripheralWord
Data Width of MemoryHalf Word

ADC采样

ADC采样相关代码参见sample.c/.h

一些采样的全局变量,256个采样值,9档采样率,初始设置采样率下标:

#define SAMPLE_RATES_NUM 9
uint16_t ADC_Value[SAMPLE_POINTS];
static const uint32_t sample_rate_list[SAMPLE_RATES_NUM] = {2560, 5120, 10240, 20480, 40960, 81920, 102400, 204800, 409600};
static int8_t sample_rate_index = 5;

ADC部分参数设置如下,其余参数大致选默认的即可:

ParametersValue
Clock Prescaler/2
Resolution12-bit
Data AligmentRight
SamplingTime Common 11.5
SamplingTime Common 21.5
N of Conversion1
External Trigger Conversion SourceTimer 1 Tigger Out Event 2
External Trigger Conversion EdgeTrigger detection on the falling edge

ADC对PA0的采样使用TIM1触发,当TIM1出现下边沿时,开始或结束采样,此时TIM1的频率即为ADC实际采样率。设置TIM1_CH3 PWM Generation No Output,prescalercounter period暂且不管,重点设置TRGO Parameters:

ParametersValue
Master/Slave ModeDisable
Trigger Event Selection TRGOReset
Trigger Event Selection TRGO2Update Event

每次开始采样前,需要根据采样值设置TIM1的AutoReload与CCR值(占空比50%即可):

uint32_t Get_SampleRate(void)
{return sample_rate_list[sample_rate_index];
}
void Set_SampleRate(uint32_t sample_rate)
{__HAL_TIM_SET_AUTORELOAD(&htim1, 64000000 / sample_rate - 1);__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, 32000000 / sample_rate);
}

从ADC采集的数据经过DMA存入数组,DMA设置如下:

ParametersValue
Channel随意
DirectionMemory to Peripheral
PriorityHigh
ModeNormal
Increment AddressMemory
Data Width of PeripheralHalf Word
Data Width of MemoryHalf Word

开启采样需要开启TIM1及ADC的DMA传输,注意开始采样前需要对ADC进行校准:

void Sample_Start(uint16_t *ADCValue)
{HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3);HAL_ADCEx_Calibration_Start(&hadc1);HAL_ADC_Start_DMA(&hadc1, (uint32_t *)ADCValue, SAMPLE_POINTS);
}void Sample_Stop(void)
{HAL_TIM_Base_Stop(&htim1);HAL_TIM_Base_Stop_IT(&htim1);HAL_ADC_Stop_DMA(&hadc1);
}

FFT

FFT及频谱相关代码参见specturm.c/.h及库`

该部分使用Adafruit_ZeroFFT库,选定做FFT点数,选择对应的窗函数,删去库多余的代码节约空间。

👉 Adafruit_ZeroFFT

通过调用ZeroFFT()即可计算FFT:

memcpy(FFT_Value, ADC_Value, sizeof(FFT_Value));
ZeroFFT((int16_t*)FFT_Value, FFT_POINT)

原代码内的FFT最后计算舍去了虚部,只保留了实部,在此参考寒假在家一起练(1) - 有信号发生器功能的简易示波器的该部分代码,改为计算幅值谱,并修正直流分量,最后将整个FFT数组做归一化即可得到归一化幅值谱:

for (i = 0; i < length; i++) 
{real = *pOut++;img = *pOut++;*pSrc++ = sqrt((int32_t)real * real + (int32_t)img * img);
}
source[0] /= 2;

通过计算后的频谱计算中心频率、获得某频率所在频谱下标调用FFT_BINFFT_INDEX即可:

float Get_ActualFreq(uint16_t *FFTValue, uint32_t sample_rate)
{return FFT_BIN(Get_SpectrumMax(FFTValue, 1), sample_rate, FFT_POINT);
}
uint8_t Get_SpectrumMax(uint16_t *FFTValue, uint8_t ignore_dc)
{uint8_t i;uint8_t temp_max_index = ignore_dc ? 2 : 0;for (i = ignore_dc ? 3 : 1; i <= FFT_POINT / 2; i++)if (FFTValue[i] > FFTValue[temp_max_index])temp_max_index = i;return temp_max_index;
}

重点计算THD,需要计算中心频率功率、高次谐波的功率和,作者计算了一定采样率下频谱包含的所有谐波的功率和,当然也可只取N次,但容易出现交互调整采样率后该谐波不在频谱内的意外(频谱所包含的频率只有Sample Rate/2)。

float Get_THDx(uint16_t *FFTValue, uint8_t ignore_dc, uint32_t sample_rate)
{uint16_t maxN_power = 0;uint16_t max_power = 0;int i = 1;  max_power = FFT_Value[FFT_INDEX(Get_SourceFreq(), sample_rate, FFT_POINT)];while(Get_SourceFreq()*(i+1) <= sample_rate/2){maxN_power += FFT_Value[FFT_INDEX(Get_SourceFreq()*(i+1), sample_rate, FFT_POINT)];i++;}return sqrtf(((float)maxN_power)/max_power);
}

最后,由于OLED显示仅128像素,只能将256点FFT的0~127归一化后作为数据显示(显示区域高90,宽128)。当对数形式显示时,将0值映射至-30dB,将最大值映射为0,由于算得的FFT数组已为幅度谱,因此只需 lg ⁡ ( x ) \lg(x) lg(x)即可,乘-30为改符号为正,并映射至0~90。

void Generate_Spectrum(uint16_t *FFTValue, uint8_t *y, uint8_t log_or_linear)
{uint8_t max_index = Get_SpectrumMax(FFTValue, 0);uint8_t i;if(log_or_linear){for(i = 0; i < FFT_POINT / 2; i++){if(FFTValue[i] > 0){y[i] = (uint8_t)(-30*log10f((1.0*FFTValue[i]/FFTValue[max_index])));}else{y[i] = GRAPH_HEIGHT-1;}}}else{for(i = 0; i < FFT_POINT / 2; i++){y[i] = (GRAPH_HEIGHT-1) * (FFTValue[max_index] - FFTValue[i]) / FFTValue[max_index];}}
}

获取按键动作

按键相关代码参见keys.c/.h

对常规按键的处理参考SCOPE-F072–基于STM32F072的多功能掌中仪器中对按键的处理,通过设置TIM14每1ms产生Update中断,并对按键扫描,可以获取按键下边沿、上边沿、长按、短点击、长按后置高、长按时间等多个动作,当按键产生动作后,执行相应操作(如改变采样率、切换示波器/频谱等)。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{        if(htim->Instance == TIM14){Key_Handle();	// 在Key_Handle()中进行按键动作的操作}
}

对于EC11旋转编码器的处理,按键部分参考前述即可,左右旋转(PB4、PA15)通过一侧IO作为输入时钟捕获,判断另一侧IO的电位即可,在此设置TIM2_CH1(对应PA15):

ParametersValue
Prescaler63
Counter Period999
Input Capture Channel 1参数如下:
ParametersValue
Polarity SelectionFalling Edge
IC SelectionDirect
Prescaler Dividion RatioNo division
Input Filter4
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM2){if(HAL_GPIO_ReadPin(KeyB_GPIO_Port, KeyB_Pin)){zoom_out = 0x00;zoom_in = 0x01;}else{   zoom_out = 0x01;zoom_in = 0x00;}}
}

OLED显示

关于OLED显示相关代码参见display.c/.hwave.c/.h及OLED库

经典128*128 4-wire SPI OLED。SPI2参数设置如下:
SPI设置
oled.c中的部分代码修改:

void OLED_WR_Byte(u8 dat,u8 cmd)
{	//u8 i;    if(cmd)OLED_DC_Set();else OLED_DC_Clr();HAL_SPI_Transmit(&hspi2, &dat, 1, 1000);OLED_DC_Set();
}
void OLED_Clear(void)
{u8 i, n;for(i = 0; i < 16; i++){for(n = 0; n < 128; n++){OLED_GRAM[n][i] = 0;//清除所有数据}}//OLED_Refresh();//更新显示
}

修改为硬件SPI写入,删去清屏函数后的刷新可使OLED屏幕显示不闪烁

wave.c主要负责获取的数据的处理,做线性映射以显示在屏幕上,还做调整Y轴显示范围等功能。

display.c主要负责显示波形,显示文字、显示其他信息等。

系统顶层

系统相关代码参见user.c/.h

主要负责监视OLED的状态与按键的动作:

typedef enum
{Oscilloscope,Distortion
}System_State;
/* ------------------- */
System_State System = Oscilloscope;
uint8_t Page_Init = 1;
uint8_t zoom_in = 0x00;  // left for zoom in, right for zooming out
uint8_t zoom_out = 0x00;
void System_Change_State(System_State State)
{System = State;Page_Init = 1;
}
void OLED_Handle(void)
{switch(System){case Oscilloscope:{if(Page_Init){Page_Init = 0;zoom_in = 0x00;zoom_out = 0x00;Oscilloscope_Init();}memset(FFT_Value, 0x0000, sizeof(FFT_Value));memset(ADC_Value, 0x0000, sizeof(ADC_Value));Source_Init();Sample_Init();Wave_View(ADC_Value, Get_SampleRate(), graph);break;}case Distortion:{if(Page_Init){Page_Init = 0;zoom_in = 0x00;zoom_out = 0x00;Distortion_Init();}Sample_Init();memcpy(FFT_Value, ADC_Value, sizeof(FFT_Value));if(ZeroFFT((int16_t*)FFT_Value, FFT_POINT)== 0){Spectrum_View((uint16_t*)FFT_Value, graph, Get_SampleRate());}break;}default:break;}
}

按键动作大致设置如下:

void Key_Handle(void)
{static Key_Type Key[3] = {0};Get_Key(Key);switch(System){case Oscilloscope:{if(Get_Rise(Key1) || Get_Long_Tri(Key1)){if(is_setting_source_freq())Inc_SourceFreq();elseInc_SourceAmp();}if(Get_Rise(Key2) || Get_Long_Tri(Key2)){if(is_setting_source_freq())Dec_SourceFreq();elseDec_SourceAmp();}if(Get_Long_Press(KeyP))    // 长按旋钮{toggle_display();System_Change_State(Distortion);}if(Get_Cont_Click(KeyP) == 2){toggle_scale();}if(Get_Rise(KeyP))    // 短按旋钮{toggle_source_setting();}if(zoom_in == 0x01){if(is_auto_scale()){Dec_SampleRate();}else{Inc_YScale();}zoom_in = 0x00;}else if(zoom_out == 0x01){if(is_auto_scale()){Inc_SampleRate();}else{Dec_YScale();}zoom_out = 0x00;}break;}case Distortion:{if(Get_Long_Press(KeyP))    // 长按旋钮{toggle_display();System_Change_State(Oscilloscope);}if(Get_Rise(KeyP))    // 短按旋钮{toggle_spectrum_yaxis();}if(zoom_in == 0x01){Inc_SampleRate();zoom_in = 0x00;}else if(zoom_out == 0x01){Dec_SampleRate();zoom_out = 0x00;}break;}default:break;}
}

在示波器状态下,按下板卡下方两个按钮,若目前是调整源频率/幅度,增大/减小频率/幅度;长按旋钮,切换至频谱/失真度曲线显示。

功能展示

OLED显示采样波形

示波器页面,中间横线显示直流电平所在处,左上角显示采样时间,中间显示目前所调整的为源频率/幅度,右上角标识当前页面。下方左侧显示信号峰值、直流偏置电压及目前Y轴显示范围;右侧显示源频率、源幅度及Y轴显示范围自动/手动调整。

示波器页面
长按旋钮,切换至频谱/失真度页面。

OLED显示频谱/失真度曲线

左上方显示横轴每格代表频率,随采样率而变化,右上角标识当前页面。下方左侧标识当前为线性/对数坐标显示,及THD;右侧为通过FFT计算的中心频率。

频谱页面
短按旋钮即可切换线性/对数坐标。

失真度曲线
👉 项目演示视频参见:基于STM32G031的失真度测试仪

项目总结

  1. 实现了PWM+板上LPF电路生成频率在DC~20KHz的正弦波信号,频率可调,并且幅度可调,从10mV~500mV,但当幅度小时生成的正弦波幅度偏差较大,当生成直流时,计算FFT会卡死;
  2. 实现了256点ADC+DMA采样,将采样的波形及其信息显示在OLED上;
  3. 实现了256点FFT、THD的计算,显示了归一化幅值谱、对数坐标显示失真度曲线。

由于此前作者纯Keil与库函数开发,此次项目接触到了CubeMX与HAL库工具链,一键生成MDK工程雀食方便,工程项目的排布省时省力。HAL库在某些包装上也有其独特优势,今后用在F407的开发试试。

👉工程文件及代码:参见【2022寒假在家练】基于STM32G031的失真度测试仪
👉 CSDN:工程源代码下载
👉 Github-KafCoppelia/DistortionMeter


http://www.ppmy.cn/news/481915.html

相关文章

STM8L使用ADC内部参考电压通道测量VDD电压

STM8L内部含有一个12位的ADC,拥有25个输入通道,包括一个内部温度传感器,一个内部参考电压 由上图可知,STM8L内部还有一个内

STM32F103RC单片机ADC1使用TIM1自动触发注入通道组的AD转换

版权声明&#xff1a;本文为博主原创文章&#xff0c;欢迎转载 https://blog.csdn.net/ZLK1214/article/details/77746783 注意&#xff1a;ADC外设最大允许的时钟频率为14MHz&#xff0c;打开ADC外设前必须先配置好分频系数&#xff01; 72MHz / 6 12MHz&#xff0c;转换速…

ADC芯片——AD7705最详细讲解(STM32)

目录 前言1. AD7705简介1.1 特性参数1.2 功能方框图1.3 引脚排列及其功能 2. 片内寄存器2.1 通信寄存器2.1.1 通讯寄存器手册说明2.1.2 通信寄存器配置&#xff08;RS20,RS10,RS00&#xff09; 2.2 设置寄存器2.2.1 设置寄存器手册说明2.2.2 设置寄存器配置&#xff08;RS20,RS…

ADC 信号调理电路设计——必要措施、实测验证和应用说明(转载)

转自周立功《面向AMetal框架与接口的编程&#xff08;上&#xff09;》 第二章 ADC 信号调理电路设计 2.3 必要措施 一个完整的采集电路框图详见图2.19&#xff0c;从传感器或信号源到最终的ADC 数据输出&#xff0c;中间需要经过输入范围调整、多通道复用等信号调理环节。除…

面试官:“同学,你做的这几个项目都不错。但怎么问QPS你就胡说呢?”

作者&#xff1a;小傅哥 博客&#xff1a;https://bugstack.cn 沉淀、分享、成长&#xff0c;让自己和他人都能有所收获&#xff01;&#x1f604; 这位同学&#xff0c;你比上一位面试者好多了&#xff0c;你的简历中做的几个项目都不错。既有业务项目&#xff0c;也有技术项目…

STM8 ADC读取数据异常问题的解决

做了一个stm8的一个测量电压电流的项目&#xff0c;发现adc通道通过一个10k电阻连接VCC&#xff0c;的时候ADC数据出来都是只有200多&#xff0c;按理说&#xff0c;10位adc应该出来1000多才对&#xff0c;由于adc出来的数据是十六位的&#xff0c;怀疑是串口发送数据的时候数据…

CC2640R2F学习笔记(27)——ADC使用

一、简介 CC2640R2F 的 ADC 是几位的、几个通道的? 12 位模数转换器 (ADC)、200MSPS、8 通道模拟多路复用器。 使用的是什么参考源&#xff1f; 参考源有两种&#xff0c;一种是内部的固定 4.3V 参考源&#xff0c;一种是内部的电池电压。 本文采用的 4.3V 固定参考源&#x…

RFSoC应用笔记 - RF数据转换器 -06- RFSoC关键配置之RF-ADC内部解析(四)

前言 RFSoC中最重要的部分是射频直采ADC和DAC的配置&#xff0c;因此了解内部相关原理结构可以帮助我们更好理解相关功能配置参数含义。本文参考官方手册&#xff0c;主要对RFSoC ADC的可编程逻辑数据接口、多频带操作、以及奈奎斯特区的操作进行介绍。 文章目录 前言RF-ADC …