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

项目介绍

本项目基于电子森林的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 np

def sin_wave(point, num):
    y = []
    for i in range(0, point):
        fz = num/2 * np.sin(np.pi/point*2*i) + num/2
        y.append(fz)
    
    return y

if __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的效率:

Parameters Value
Channel 随意
Direction Memory to Peripheral
Priority Medium
Mode Circular
Increment Address Memory
Data Width of Peripheral Word
Data Width of Memory Half 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部分参数设置如下,其余参数大致选默认的即可:

Parameters Value
Clock Prescaler /2
Resolution 12-bit
Data Aligment Right
SamplingTime Common 1 1.5
SamplingTime Common 2 1.5
N of Conversion 1
External Trigger Conversion Source Timer 1 Tigger Out Event 2
External Trigger Conversion Edge Trigger detection on the falling edge

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

Parameters Value
Master/Slave Mode Disable
Trigger Event Selection TRGO Reset
Trigger Event Selection TRGO2 Update 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设置如下:

Parameters Value
Channel 随意
Direction Memory to Peripheral
Priority High
Mode Normal
Increment Address Memory
Data Width of Peripheral Half Word
Data Width of Memory Half 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):

Parameters Value
Prescaler 63
Counter Period 999

Input Capture Channel 1参数如下:

Parameters Value
Polarity Selection Falling Edge
IC Selection Direct
Prescaler Dividion Ratio No division
Input Filter 4
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();
                else
                    Inc_SourceAmp();
            }
            if(Get_Rise(Key2) || Get_Long_Tri(Key2))
            {
                if(is_setting_source_freq())
                    Dec_SourceFreq();
                else
                    Dec_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的失真度测试仪
👉 项目文件
👉 Github项目

版权声明:本文为CSDN博主「丶葉SiR」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_46422143/article/details/122949391

项目介绍

本项目基于电子森林的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 np

def sin_wave(point, num):
    y = []
    for i in range(0, point):
        fz = num/2 * np.sin(np.pi/point*2*i) + num/2
        y.append(fz)
    
    return y

if __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的效率:

Parameters Value
Channel 随意
Direction Memory to Peripheral
Priority Medium
Mode Circular
Increment Address Memory
Data Width of Peripheral Word
Data Width of Memory Half 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部分参数设置如下,其余参数大致选默认的即可:

Parameters Value
Clock Prescaler /2
Resolution 12-bit
Data Aligment Right
SamplingTime Common 1 1.5
SamplingTime Common 2 1.5
N of Conversion 1
External Trigger Conversion Source Timer 1 Tigger Out Event 2
External Trigger Conversion Edge Trigger detection on the falling edge

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

Parameters Value
Master/Slave Mode Disable
Trigger Event Selection TRGO Reset
Trigger Event Selection TRGO2 Update 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设置如下:

Parameters Value
Channel 随意
Direction Memory to Peripheral
Priority High
Mode Normal
Increment Address Memory
Data Width of Peripheral Half Word
Data Width of Memory Half 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):

Parameters Value
Prescaler 63
Counter Period 999

Input Capture Channel 1参数如下:

Parameters Value
Polarity Selection Falling Edge
IC Selection Direct
Prescaler Dividion Ratio No division
Input Filter 4
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();
                else
                    Inc_SourceAmp();
            }
            if(Get_Rise(Key2) || Get_Long_Tri(Key2))
            {
                if(is_setting_source_freq())
                    Dec_SourceFreq();
                else
                    Dec_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的失真度测试仪
👉 项目文件
👉 Github项目

版权声明:本文为CSDN博主「丶葉SiR」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_46422143/article/details/122949391

生成海报
点赞 0

丶葉SiR

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

L298N电机驱动的使用

前言 博主为某大学电气专业大学生,以学习为目的写下该文,内容主要为以51单片机为例简单介绍L298N电机驱动模块的使用。内容若有不适,欢迎指正、补充和联系。 提示:以下是本篇文章正文内容