寒假在家一起练(1) - 有信号发生器功能的简易示波器
基于STM32G031的简易示波器与信号发生器,具有两路输入和一路输出。示波器包含波形显示、频谱显示、自动缩放、触发等功能;信号发生器可以输出频率分量最高至2kHz的正弦波、方波和三角波,且频率、幅度可调。
标签
嵌入式系统
测试
显示
DDS
氢化脱氯次氯酸
更新2021-02-27
9380

1 项目需求

  • 完成对板上音频信号的采集和波形显示,可以通过手机播放音乐或App产生音频信号的方式提供声音信号源,通过板上电路的放大、MCU中ADC的采集以后将波形显示在OLED屏幕上,可以通过板上按键的操作在两个方向(横轴 - 时间;纵轴 - 幅度)来扩展、压缩波形的显示,按键的功能可自行定义;
  • 实现信号发生器的功能,能够产生2KHz以内的正弦波、三角波、方波三种常用波形,通过按键的操作能够实现频率可调、幅度可调,通过调整板上的R、C的值,可以最高生成200KHz的模拟信号;
  • 能够通过Ain管脚测量外部模拟信号(0-3.3V,DC-200KHz),并能够对外部的周期性波形测量其周期和峰-峰值;
  • 能够对采集到的信号进行FFT变换,并在屏幕上显示其基频及低次谐波(比如2、3、4、5次)的分量。

2 完成的功能及达到的性能

2.1 波形显示

显示波形时,按下L提高采样率,按下R降低采样率,采样率取值范围为1kHz、2.5kHz、5kHz、10kHz、25kHz、50kHz、100kHz、250kHz、500kHz、1MHz,通过改变采样率来实现横轴的缩放。

Y轴(幅度范围)默认为自动调整,即程序自动根据采样序列调整Y轴中心电压值和缩放范围,使波形完整显示在屏幕上。通过菜单可以改为手动模式,即手动调整Y轴中心电压值和Y轴缩放范围。

左下角显示波形参数,可以显示时间轴分度值、信号峰峰值、直流分量和频率。

正下方显示当前状态,包含输入通道、触发状态和前述的Y轴缩放方式(A:自动缩放,MO (Manual Offset):U/D按键调整Y轴中心电压值,MS (Manual Scale):U/D按键调整Y轴缩放范围。

按下OK键可以暂停波形刷新,再按可以继续刷新。

FvdVKSxe22uBcvc0A8i2rEL85DX3

2.2 触发显示和触发菜单

程序默认为上升沿触发,触发电平为1.68V。显示波形且触发开启时,屏幕正下方显示当前触发边沿(上升沿、下降沿)和触发状态(箭头点亮为触发成功、背景点亮为触发失败)。

长按R键打开触发菜单,在触发菜单中可以开启/关闭触发,选择触发边沿,选择自动触发还是单次触发。

FowUvzgeQR_zQ_pnwY9UZWU0Q-8J

2.3 示波器菜单

长按OK键打开示波器菜单,示波器菜单共有4项,分别是:波形/频谱显示切换、Y轴缩放方式、波形参数切换、通道切换(麦克风与板上信号输入)。LRUD四个按键用来对上述四项功能进行切换。

FpZ25gDo-R0meVUeazPWw4U9uM_Y

2.4 频谱显示

通过菜单切换至频谱显示时,屏幕显示信号的频谱,显示频率范围为直流至采样频率的一半。同样按下L提高采样率,按下R降低采样率。左下角显示频率轴分度值。

FoZqMrOaIJG9QyqOg2Y3EjW5q-yt

2.5 信号输出

长按L键打开输出菜单,在输出菜单中,可以开启/关闭信号输出,增加/降低输出信号的频率(步长100Hz,上限2kHz)、峰峰值(步长0.1V,上限3.3V)和调整输出波形(正弦波、三角波、方波)。

Fqt3aqpPqaSc74gDLaVrGcsnATDQ

3 实现思路

  • ADC对模拟输入进行采样,采样由定时器触发,采样结果由DMA搬运;
  • 将采样得到的ADC量化值映射到屏幕坐标点上,实现波形显示;
  • 按下按键调整采样频率,实现波形在时间轴上的扩展与压缩;
  • 对采样序列进行FFT变换,绘制频谱;
  • 信号参数的显示,如峰峰值、直流分量、信号频率等;
  • 输出PWM波并通过RC低通滤波实现方波、正弦波、三角波的生成,通过按键改变PWM波的频率与占空比,从而改变输出信号的频率和幅度。

4 实现过程

4.1 程序流程图 

FhR0netVswEmJYP2ykWMKIbn7ev4

注:每个框图右下角名称为执行该功能的主要文件

4.2 ADC对数据进行采样

为了方便进行FFT计算,ADC共采集256个采样点。每次ADC转换由定时器1触发,触发频率最高为1MHz,即ADC采样率最高为1Msps。ADC的转换结果直接由DMA搬运至内存。

ADC转换开始函数(定义位置:sample.c,调用位置:main.c):

/**
  * @brief      Start a new sample sequence.
  * @param[in]  ADCValue  Array to store incoming sample values.
  * @retval     None
  */
void start_sample(uint16_t *ADCValue)
{
    HAL_Delay(1);
    HAL_ADCEx_Calibration_Start(&hadc1);
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)ADCValue, SAMPLE_POINTS);
}

256次转换结束后进入中断,置位结束标志位,进入后续的数据处理程序。

ADC转换结束中断回调函数(定义位置:adc.c):

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
  if(hadc == &hadc1)
  {
    finish_sample();
  }
}

4.3 采样结果的处理

得到256个采样的ADC量化值后,根据触发电平选择波形起始点,返回起始点在数组中的下标,显示从起始点开始的100个点。

波形触发部分代码(定义位置:wave.c,调用位置:app.c,其中total_points=256, GRAPH_WIDTH=101):

/**
  * @brief       Wave trigger.
  * @param[in]   ADCValue  Array of sampled ADC values.
  * @param[in]   total_points  Total sampled points.
  * @retval      Index of the trigger start point(>1). 0 means trigger off or failed.
  */
uint16_t trigger(uint16_t *ADCValue, uint16_t total_points)
{
    uint16_t i;
    uint16_t trigger_value = VOL2ADC(1.68);

    if (!is_trigger_on())
        return 0;

    for (i = 1; i < total_points - GRAPH_WIDTH + 2; i++)
    {
        if (get_trigger_edge())  // falling edge
        {
            if (ADCValue[i-1] > trigger_value && ADCValue[i] <= trigger_value)
            {
                trigger_success();
                if (is_trigger_single())
                    pause();
                return i;
            }
        }
        else
        {
            if (ADCValue[i-1] <= trigger_value && ADCValue[i] > trigger_value)
            {
                trigger_success();
                if (is_trigger_single())
                    pause();
                return i;
            }
        }
    }

    trigger_fail();
    return 0;
}

取起始点后100个采样值使其显示在OLED屏幕上(一次性刷新)。为此需要将ADC量化值与OLED屏幕上的坐标进行线性映射。在自动模式(自动缩放y轴)中,程序自动找出量化值中的最大最小值,并使最大最小值也能不超出绘制范围以外,这样屏幕就可以显示完整的波形。

自动缩放y轴代码(定义位置:wave.c,调用位置:app.c):

/**
  * @brief      Automatically find the central/max/min voltage on y-axis.
  * @param[in]  ADCValue  Array of sampled ADC values.
  * @note       The function calculates the min/max voltage of the sampled signal,
  *             then find a proper scale voltage and a central voltage on y-axis.
  * @retval     None
  */
void auto_scale(uint16_t *ADCValue)
{
    uint16_t a_max_value, a_min_value, a_pp_value;
    float exact_voltage, floor_voltage, ceil_voltage;
    get_max_min_pp_value(ADCValue, &a_max_value, &a_min_value, &a_pp_value);
    voltage_range_auto_select(ADC2VOL(a_pp_value/2));

    exact_voltage = ADC2VOL(a_max_value + a_min_value) / 2;
    floor_voltage = (uint8_t)(ADC2VOL((a_max_value + a_min_value)*5)) / 10.0;  //keep one decimal
    ceil_voltage = floor_voltage + 0.1;

    // round center_voltage
    volt_on_y_axis.center_voltage = ceil_voltage - exact_voltage < exact_voltage - floor_voltage ? ceil_voltage : floor_voltage;
    volt_on_y_axis.max_voltage = volt_on_y_axis.center_voltage + v_scale_list[v_scale_index];
    volt_on_y_axis.min_voltage = volt_on_y_axis.center_voltage - v_scale_list[v_scale_index];
}

坐标映射代码(定义位置:wave.c,调用位置:app.c):

/**
  * @brief       Generate y-coordinates of the wave.
  * @param[in]   ADCValue  Array of sampled ADC values.
  * @param[out]  y  Y-coordinate array of the wave.
  * @note        The function map ADCValues to OLED y coordinates.
  * @retval      None
  */
void generate_wave(uint16_t *ADCValue, uint8_t *y)
{
    // Quantize y-axis min/max/central voltages to ADC values.
    int16_t a_max_value = VOL2ADC(volt_on_y_axis.max_voltage);
    int16_t a_min_value = VOL2ADC(volt_on_y_axis.min_voltage);
    uint8_t i;

    // Linearly map every ADC value to its coordinate.
    for (i = 0; i < GRAPH_WIDTH - 1; i++)
    {
        if (ADCValue[i] <= a_max_value && ADCValue[i] >= a_min_value)
            y[i] = (GRAPH_HEIGHT - 1) * (a_max_value - ADCValue[i]) / (a_max_value - a_min_value) + GRAPH_START_Y;
        else if (ADCValue[i] > a_max_value)
            y[i] = GRAPH_START_Y;
        else if (ADCValue[i] < a_min_value)
            y[i] = GRAPH_HEIGHT + GRAPH_START_Y - 1;
    }
}

波形显示代码(定义位置:display.c,调用位置:app.c):

/**
  * @brief      Display wave on OLED.
  * @param[in]  y  Y-coordinate array of the wave.
  * @retval     None
  */
void display_wave(const uint8_t *y)
{
    uint8_t x;
    for (x = GRAPH_START_X; x < GRAPH_WIDTH - 1; x++)
        OLED_DrawLine(x, y[x-GRAPH_START_X], x + 1, y[x-GRAPH_START_X+1], 1);
    OLED_DrawPoint(x, y[x-GRAPH_START_X], 1);
}

在手动模式中,可以手动调节y轴的缩放范围和y轴中心电压值,但此时波形不一定会完整显示。得到采样点坐标后,使用OLED的绘制直线函数,连接屏幕上各个离散的点,就可以得到信号的波形。

当需要显示频谱时,就需要对所有的ADC的量化值进行256点FFT变换,由于FFT变换结果关于中心点对称,且屏幕x方向分辨率为128点,所以保留FFT需要为0~127的结果,进行线性映射后显示在屏幕上。

FFT的代码定义在fftutil.c中,对变换结果的处理及显示分别定义在spectrum.c和display.c中。

4.4 信号发生器

板上有一个1Kohm的电阻和10nF的电容构成的低通滤波器,截止频率为1.6KHz。若在该输出端输出频率足够的PWM信号,则输出电压大小就和PWM的占空比成正比。通过改变PWM的占空比就可以调节输出电压波形。通过实验可知,当信号的每一个周期由500个PWM脉冲组成时,信号的纹波较小。

以正弦信号为例,在程序外,在电脑中生成一个正弦信号,并在一个周期中进行500次采样,根据电压和PWM占空比的正比关系可以计算出500个PWM脉冲的占空比。将其定义为长度为500的数组写入程序。程序中使能PWM的DMA通道,这样就可以在每个PWM脉冲结束后自动将数组中的元素载入定时器输出比较寄存器,从而改变占空比。低通滤波器再将STM32产生的PWM脉冲转变为模拟信号,即可重新生成正弦波。方波和三角波同理。

开启PWM和DMA代码(定义位置:source.c,调用位置:app.c,其中SIGNAL_LENGTH=500):

/**
  * @brief  Start signal output at Aux.
  * @retval None
  */
void start_output(void)
{
    HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_2, (uint32_t *)output_wave_value, SIGNAL_LENGTH);
}

信号的幅度调节可以直接对上述数组每个元素乘一个常数来实现;频率调节首先要调节定时器的自动重载值(ARR),改变PWM的频率。为保证幅度不变,数组中每个元素也要同比例缩放。

5 遇到的主要难题

5.1 中断与DMA

项目共有两处使用DMA,分别用于储存ADC采样结果和调整输出PWM定时器的自动重载值(ARR)。如果用中断处理数据而非DMA,则会产生以下问题:

若在ADC转换完成中断中读取转换结果,则在一次采样序列(256点)中,中断频率过于频繁,且由于中断耗时,无法得到很高的采样率,最高只能达到几十kHz。若使用DMA,则只需在整个采样序列结束后进入中断,不会对采样造成影响。

若使用PWM中断更新自动重载值,中断耗时会使PWM频率产生偏差,且会对OLED屏幕的SPI时序造成影响,导致屏幕无法正常显示。若使用DMA更新自动重载值,则不需要PWM中断,更新耗时相比于中断有很大改善。

综上所述,在频率较高或需要频繁更新数据的情况,中断会带来各种各样的问题,而DMA则可以高效完成任务。

5.2 RAM和Flash大小(FFT优化)

项目使用的FFT算法根据Adafruit ZeroFFT修改而来。该算法最高可支持4096点FFT,其旋转因子表、窗函数表和信号序列数组占用空间极大。而本项目使用的STM32G031G8只有64K的Flash和8K的RAM,资源极为有限,无法直接运行ZeroFFT。

为此需要对ZeroFFT的代码进行优化。该项目只需256点FFT,删去256点之外的部分,缩短查找表,能极大减小RAM和Flash占用。

具体的优化步骤:

  • 将Adafruit_ZeroFFT.h中的宏定义ZERO_FFT_MAX改为512。(对应256点FFT)
  • 删去fftutil.c中ZeroFFT函数所有其他点数的FFT代码,只保留256点FFT的代码。同样删去窗函数中256点以外的部分和窗函数查找表。
  • 此时fftutil.c中只调用了arm_common_tables.c中armBitRevTable和twiddleCoefQ15两个查找表,删去其他所有数组。
  • 在fftutil.c中所有调用armBitRevTable和twiddleCoefQ15查找表的代码下面添加printf,用PC运行FFT程序,打印调用的下标。
  • 以twiddleCoefQ15数组为例,原长度为6144;对于256点FFT,只有其中384个值被调用。PC中编写一个临时程序,根据调用的下标,用printf打印一个新的长度为384的查找表替换掉原来的。另一个查找表同理。
  • fftutil.c中部分变量代表查找表的步进值,查找表改变后这些步进值也要改变。
  • 此时FFT的代码应该就可以在STM32G0上运行了~

此外,由于Flash和RAM的资源有限,在FFT之外的其他很多地方也需要对空间进行优化,比如删去oled不需要的字库等。

5.3 PWM输出频率

由于电容的充放电,由PWM经过低通滤波输出的信号会有锯齿,信号幅度较低时锯齿更为明显,并会造成波形显示的不稳定。开始时输出信号一个周期内有50个PWM脉冲,即PWM的频率是信号频率的50倍,当信号幅度较低时锯齿极为明显,对输出波形造成极大干扰。将一个周期内PWM脉冲数提升至500,锯齿密度变大,同时幅度减小,对输出信号的干扰也减小。但同时储存输出信号幅度信息的查找表也变大10倍,消耗了更多的空间。

6 未来的计划建议

该项目已经成功实现了简易示波器和信号发生器的功能,并达到了预期指标。然而通过更换硬件,还有许多可以提升与扩展的地方:

  • 板上的OLED屏幕分辨率较低,无法显示信号细节与更多信息。可以使用分辨率更高的屏幕,或将波形信息直接发送给上位机,由上位机进行显示。
  • 主控芯片STM32G031的资源有限。可以更换更好的主控芯片,来提高采样率,采样点数等从而实现更高的性能。
  • 可以对输入信号进行衰减,从而增大输入信号的电压范围。
  • 增加模拟输入的通道,并添加波形的数学运算功能,如波形之间的加减。
  • 改变输出端的RC值,扩展输出信号频率范围。

不更换硬件可以提升与扩展的地方(懒得做的部分):

  • 自动/手动调整触发电平。
  • 改变输入信号耦合方式(直流/交流耦合)。
  • 对输入信号进行数字滤波。
  • 信号源实现更高的频率分辨率。
附件下载
code.zip
工程文件
STM32_Oscilloscope.elf
可以直接烧录的程序
团队介绍
中国科学技术大学
团队成员
王赫男
中国科学技术大学电子信息工程专业
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号