蓝桥杯项目实战练习(一)—— 简易信号发生器

OG比特 发布于 2 天前 114 次阅读


前言

该项目主要利用了STM32的DAC和DMA模块,整体思路是:利用不同的波形生成函数计算每一个波点对应的电压值并储存在数组中,接着利用DMA将数据循环写入DAC的寄存器中,最终由DAC输出为模拟信号。在代码构建过程中,我先完成了固定频率和振幅的正弦波的输出,接着引入两个变量来调节频率和振幅,然后修改波形生成函数以生成其它三种波形(同样参数可调),最后完成了按键控制和LCD显示部分。

文章如果有任何纰漏或者可以改进的地方,欢迎友好指正交流😘

一、项目简介

该项目是一个简易的信号发生器,可以产生正弦波、矩形波、三角波、锯齿波,共四种波形。波形的频率和振幅可调,频率的调节范围是1kHz-5kHz,振幅的调节范围是0.6V-1.6V。参数通过按键调节,同时通过LCD来显示具体信息。

软件环境

  • Windows11
  • STM32CubeMX v6.13
  • Keil.STM32G4xx_DFP.1.6.1
  • AC6

硬件环境

  • STM32G431 RBT6
  • 蓝桥杯嵌入式硬件平台

二、代码构建

1.产生固定参数的正弦波

此一步是为了先行跑通基本的波形发生功能,确保DAC和DMA模块的配置正确。

配置CubeMX

首先是时钟、按键、led一条龙,熟悉流程的朋友请在右侧大纲栏跳转至△配置DAC和定时器触发

输入频率为设置为24MHz,系统频率设置为80MHz

△配置DAC、DMA和定时器触发

Trigger触发源可以是任意可选定时器的更新事件,不同定时器在配置时操作略有差别,但核心都是将Trigger Event Selection调整为Update Event

配置波形函数

首先引入math.h头文件,以使用sin()函数进行数值计算,再定义一个数据缓存数组buff[400],用来存储DMA 向DAC转运的数据。

#include "math.h"

uint16_t buff[400] = {0};

int main(void)
{
	......//省略前面的内容
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_DAC1_Init();
  MX_TIM7_Init();
  /* USER CODE BEGIN 2 */
	HAL_TIM_Base_Start(&htim7);//开启定时器
	for(uint16_t i = 0;i < 400;i++)
	{
		buff[i] = 2000+2000*(sin(2*3.14/400*i));//计算波点对应值并装入数组
	}
	HAL_DAC_Start_DMA(&hdac1,DAC_CHANNEL_1,(uint32_t *)&buff[0],400,DAC_ALIGN_12B_R);//开启DAC和DMA转运
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		GPIOA->ODR ^= 1 << 5;
    HAL_Delay(4);//延时满足数据计算、写入的时间需求
  }

编译工程并烧录,得到频率为1kHz,振幅约为1.6V的正弦波

2.引入变量调整频率和振幅

定义变量amplitude_vfrequency_khzamplitude_u16来代表幅值和频率,更新后代码如下:

float amplitude_v   = 1.0f;
float frequency_khz = 1.0f;
uint16_t amplitude_u16;//将amplitude_v转换为uint16_t类型

HAL_TIM_Base_Start(&htim7);//开启定时器
	amplitude_u16 = (uint16_t)(amplitude_v * 4095/3.3);//4095:12位DAC最大输出值;  3.3:参考电压;
	float omega = 2 * 3.1415926f * frequency_khz / 400.0f;//提前计算用到的浮点数,使代码运行更流畅
	for(uint16_t i = 0;i < 400;i++)
	{
		buff[i] = amplitude_u16 + amplitude_u16 * (sin(omega * i));
	}
HAL_DAC_Start_DMA(&hdac1,DAC_CHANNEL_1,(uint32_t *)&buff[0],400,DAC_ALIGN_12B_R);

调节amplitude_vfrequency_khz的值,发现波形的振幅和相位发生相应改变。

类似的,写出另外三个波形的发生函数,并添加一个变量mode,用它的值来代表不同波形,最终的波形生成代码如下。

uint8_t mode = 0;

void switch_mode(void)
{
    amplitude_u16 = (uint16_t)(amplitude_v * DAC_MAX_VALUE / DAC_REF_VOLTAGE);
    float omega = 2 * 3.1415926f * frequency_khz / 400.0f;

    switch (mode)
    {
        case 0: // 正弦波
            for (uint16_t i = 0; i < 400; i++)
            {
                float sine_value = sin(omega * i);
                sina[i] = amplitude_u16 + (uint16_t)(amplitude_u16 * sine_value);
            }
            break;

        case 1: // 方波
            for (int i = 0; i < 400; i++)
            {
                sina[i] = amplitude_u16 + ((sin(omega * i) > 0) ? amplitude_u16 : -amplitude_u16);
            }
            break;

        case 2: // 三角波
            for (int i = 0; i < 400; i++)
            {
                float t = (i * frequency_khz) / (float)400;
                sina[i] = amplitude_u16 + amplitude_u16 * (2.0 * fabs(2.0 * (t - floor(t + 0.5))) - 1.0);
            }
            break;

        case 3: // 锯齿波
            for (int i = 0; i < 400; i++)
            {
                float t = (i * frequency_khz) / (float)400;
                sina[i] = amplitude_u16 + amplitude_u16 * (2.0 * (t - floor(t)) - 1.0);
            }
            break;

        default:
            break;
    }

    HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t *)&sina[0], 400, DAC_ALIGN_12B_R);
}

后续可以通过按键改变mode的值,来实现按键切换波形。

3.按键

功能设计

按键B1切换不同波形,B2选择要调整的参数:amplitude_vfrequency_khz,B3控制数值增大,B4控制数值减小。

代码实现

首先要在工程中添加自己创建的key.ckey.h

这里使用了检测下降沿的方法来识别按键状态,优点是代码清晰简洁,非阻塞,不用做专门的消抖处理。

首先在key.h中定义按键结构体

struct key
{
    uint8_t sta;
    uint8_t last_sta;
    uint8_t short_falg;
};

key.c中定义四个按键的结构体bkeys[4],然后编写按键扫描程序

struct key bkeys[4] = {0};

void key_serv(void)
{
	for (size_t i = 0; i < 3; i++)
    {
        bkeys[i].sta = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0<<i);//记下当前电平
        if (bkeys[i].sta == 0 && bkeys[i].last_sta == 1)//之前为高,现在为低,代表下降沿
        {
            bkeys[i].short_flag = 1;//置短按标志位为一
        }
        bkeys[i].last_sta = bkeys[i].sta;//将本次运行的电平记录为last_sta,下次运行时会作为上次的电平进行判断
    }
    bkeys[3].sta = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);//以下同理
    if (bkeys[3].sta == 0 && bkeys[3].last_sta == 1)
    {
        bkeys[3].short_flag = 1;
    }
    bkeys[3].last_sta = bkeys[3].sta;
}

我们还需要一个变量来表示当前选择要更改的参数,我们将它定义为para,默认为0,为0代表选中amplitude_v,为1代表选中frequency_khz

uint8_t para = 0;
extern uint8_t mode;
extern float amplitude_v;
extern float frequency_khz;

void key_proc(void)
{
    if (bkeys[0].short_flag)//按键B1
    {
        
        mode++;
        if (mode > 3)
        {
            mode = 0;
        }//使mode在0-3之间循环切换
        bkeys[0].short_flag = 0;
    }
    if (bkeys[1].short_flag)//按键B2
    {
        para = !para;//按键按下反转para,切换选中参数
        bkeys[1].short_flag = 0;
    }
    if (bkeys[2].short_flag)//按键B3
    {
        if (para == 0)//判断选中参数
        {
            if (amplitude_v < 1.6f)//参数最大值
            {
                amplitude_v += 0.1f;//参数增加
            }
        }
        else
        {
            if (frequency_khz <5.0f)//参数最大值
            {
                frequency_khz += 1.0f;//参数增加
            }
        }
        bkeys[2].short_flag = 0;
    }
    if (bkeys[3].short_flag)//按键B4
    {
        if (para == 0)//判断选中参数
        {
            if (amplitude_v > 0.6f)//参数最小值
            {
                amplitude_v -= 0.1f;//参数减小
            }
        }
        else
        {
            if (frequency_khz > 1.0f)//参数最小值
            {
                frequency_khz -= 1.0f;//参数减小
            }
        }
        bkeys[3].short_flag = 0;
    }
}

将写好的按键函数在key.h中声明,然后放在main.c的主函数中执行

在这里,我们想要让key_serv()执行的慢一点,这并不影响正常的按键检测,还能避免按下时检测到多次下降沿,起到的一定的消抖作用,更加保险。代码实现上我们利用时间戳的方法,只需要使用滴答定时器和一个记录时间变量的变量就可以完成,十分节省软件资源。

#include "key.h"

uint32_t key_update_kick = 0;

while (1)
  {
	if(HAL_GetTick() - key_update_kick >=10)
	{
		key_serv();//10ms进行一次按键扫描
        key_update_kick = HAL_GetTick();
	}
   	key_proc();
	switch_mode();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	GPIOA->ODR ^= 1 << 5;
    HAL_Delay(4);//延时满足数据计算、写入的时间需求
  }

至此,我们已经实现利用按键控制波形的种类、振幅、频率, 项目只剩最后一步:使用lcd显示信息。

4.LCD显示

计划在lcd上显示波形种类、振幅、频率的信息,同时需要编写高亮程序,以指示当前调整的参数是振幅 or 频率。

导入并修改lcd驱动

蓝桥杯提供的选手包里有现成的lcd驱动,我们只需将其添加进工程即可。点击这里可以下载第15届蓝桥杯电子赛的官方选手包.

压缩包解压完成后按照...\DP2024_ES(嵌入式)\BSP\LCD_Driver\MDK5_LCD_HAL的路径找到lcd的HAL库驱动,分别在路径下的srcinc中找到lcd.clcd.hfont.h,并将他们导入至工程中。

由于蓝桥杯嵌入式平台的led和lcd的引脚存在共用的问题,下面对lcd.c进行一些修改,避免lcd刷新时影响led的状态

找到LCD_Init()的定义,在函数体的首尾各添加一行代码。

void LCD_Init(void)
{
	uint16_t temp = GPIOC->ODR;//暂存GPIOC寄存器状态
    
    LCD_CtrlLinesConfig();
    dummy = LCD_ReadReg(0);

    if(dummy == 0x8230)
    {
        REG_8230_Init();
    }
    else
    {
        REG_932X_Init();
    }
    dummy = LCD_ReadReg(0);
    
	GPIOC->ODR = temp;//恢复之前的状态
}

同理将LCD_Clear(u16 Color)LCD_DisplayStringLine(u8 Line, u8 *ptr)函数也进行相同的修改。

然后在主函数中初始化lcd

#include "lcd.h"  

while(1)
{
  ...//省略
  /* USER CODE BEGIN 2 */
  HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);//关闭PD2,防止led更新
  LCD_Init();
  LCD_Clear(Black);
  LCD_SetBackColor(Black);
  LCD_SetTextColor(White);
  /* USER CODE END 2 */
  ...//省略
}

下面正式开始lcd函数的编写

首先我们需要再工程中新建自己的lcd文件My_lcd.cMy_lcd.h

My_lcd.h中引用lcd.hstdio.h,以使用lcd驱动中的函数以及sprint函数

#ifndef __MY_LCD_H
#define __MY_LCD_H

#include "main.h"
#include "lcd.h"
#include "stdio.h"


#endif 

下面编写源文件,首先定义一个10*20二维数组或者说一个字符串数组,来存放要显示在lcd上的内容。相较于使用只保存一行内容的一维数组,使用二维数组的优点是可以一次性提前将要显示的内容编辑好,然后利用for循环连续打印,同时可以通过行号来快速索引某一行内容,方便其他函数引用,个人感觉在编写高亮程序时也更方便了,缺点就是比一维数组更占空间。

我们还需要一个字符串数组来存放四种波形的名称,后续可以利用变量mode来索引到对应的波形名称。

char str[10][20];
const char* wave_symbols[] = {"Sina", "Square", "Tri", "Saw"};

extern float amplitude_v;
extern float frequency_khz;
extern uint8_t mode;

void lcd_text_init(void)
{
    sprintf(str[1], "   Wave Generator    ");
    sprintf(str[3], "     Wave: %s        ", wave_symbols[mode]);
    sprintf(str[5], " Amplitude: %.2fV    ", amplitude_v);
    sprintf(str[7], " Frequency: %.2fkHz  ", frequency_khz);
}

然后我们来编写高亮程序

void lcd_high_light_line(uint8_t Linex)
{
    for (uint8_t i = 0; i < 10; i++)
    {
        if (i != Linex)
        {
            LCD_DisplayStringLine(Line0 + 24 * i, (uint8_t *)str[i]);//非高亮行时正常显示
        }
        else
        {
            LCD_SetBackColor(Blue);//准备打印高亮行时,先将背景调成蓝色,打印完成后再调回黑色
            LCD_DisplayStringLine(Line0 + 24 * Linex, (uint8_t *)str[Linex]);
            LCD_SetBackColor(Black);
        }
    }
}

最后是指定高亮的条件,并将程序整合一下。

extern uint8_t para;

void lcd_high_light_ctrl(void)
{
    if (para ==0)
    {
        lcd_high_light_line(5);//参数为零时高亮振幅信息所在行
    }
    else
    {
        lcd_high_light_line(7);//参数为一时高亮频率信息所在行
    }
}
void lcd_proc(void)
{
    lcd_text_init();
    lcd_high_light_ctrl();
}

My_lcd.h中声明lcd_proc()函数,然后将其放在主函数的循环中,利用和定时执行按键扫描相同的方法,我们将lcd固定为100ms刷新一次。

#include “My_lcd.h”

while (1)
  {
		if(HAL_GetTick() - key_update_kick >=10)
		{
			key_serv();
			key_update_kick = HAL_GetTick();
		}
		key_proc();
		switch_mode();
		if(HAL_GetTick() - lcd_update_kick >=100)
		{
			lcd_proc();//100ms刷新一次lcd
			lcd_update_kick = HAL_GetTick();
		}
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		GPIOA->ODR ^= 1 << 5;
    HAL_Delay(4);//延时满足数据计算、写入的时间需求
  }
  /* USER CODE END 3 */
}

三、项目下载

项目工程文件下载地址:

百度网盘:https://pan.baidu.com/s/1iULpvitGC22t_2hby6gbaw?pwd=9emc 提取码: 9emc

Github: 0verGeek/WaveGenerator_demo1: 蓝桥杯项目实战练习(一)—— 简易波形发生器

Gitee: WaveGenerator_demo1: 蓝桥杯项目实战练习(一)—— 简易波形发生器


看到下方那个萌萌的猪猪存钱罐了吗?如果这篇文章让你有所收获,不如请我喝杯咖啡吧~你的支持是我继续码字的动力,感激不尽!🐷💖