2021暑假一起练-用STM32F072设计的简易函数发生器和PWM波产生器
基于STM32F072设计的简易函数发生器和PWM波产生器,函数发生器可调频率,幅值,直流偏置,输出正弦波、三角波、锯齿波;PWM发生器可调频率占空比
标签
嵌入式系统
数字逻辑
显示
Old_ChoBo
更新2021-09-12
1667

一、      任务描述

项目2 制作简易信号发生器

  1. 通过STM32F072的DAC产生正弦波、三角波等常用波形,输出到Wav管脚
  2. 通过STM32F072的内部定时器产生可调周期、可调占空比的PWM信号,输出到PWM管脚
  3. 可以通过按键改变Wav信号的波形、频率、幅度、直流偏移,改变PWM信号的频率和占空比
  4. 在LCD上显示波形信息以及当前的参数、控制菜单

二、      完成情况

  1. 能通过Wav管脚输出正弦波,三角波,方波,可调节频率,幅值,直流偏置,输出电压范围-3.6V~3.6V
  2. 能通过PWM管脚输出可调频率和占空比的PWM波
  3. 能够通过屏幕显示当前输出波形的形式与参数,可通过按键切换光标对波形数据进行实时修改

三、使用说明:

   以屏幕所在面为正面。从左至右分别为Key_1, Key_2, 拨盘。拨盘可按下,左拨,右拨

   Key_1: 按下切换模式,AWG(任意波形生成器)和PWM波发生器

   Key_2: 按下切换纵向光标

   按下拨盘:切换横向光标

   左拨拨盘:数据减小,切换单位

   右拨拨盘:数据增大,切换单位

对于频率,幅值,直流偏置,占空比,可手动调节每一数字位。频率可切换到单位显示来改变Hz和kHz

四、      硬件资源配置

DAC:

Parameter Settings:

   Output Buffer: Enabled      //需要开启,否则无法得到准确的电压输出

   Trigger:Timer 3 Trigger Out Event    //使用TIM3计数溢出事件驱动DAC输出,从而控制频率

   Wave Generation Mode: Disabled

DMA Settings:

   配置循环输出(Circular), 内存地址递增,Data Width半字(Half Word),外设地址不递增,Data Width为字(Word)

TIM2:

配置Channel3位PWM波输出模式。固定Counter Period为99,从而改变PSC的值即可实现修改占空比。

TIM3:

  Parameter Settings->Trigger Output Parameters->Trigger Event Selection 选择为 Update Event 否则无法触发DAC输出

SPI:

   Mode:Transmit Only Master

   Frame Format:Motorola

   Data Size: 8 bits

   Prescaler: 4

   其余保持默认设置。同时需要配置LCD_RSTn和LCD_DC为IO口输出,作为复位和读写控制

LCD驱动参考程序:

https://blog.csdn.net/Mculover666/article/details/100151218?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162868131516780265441197%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=162868131516780265441197&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~baidu_landing_v2~default-5-100151218.pc_search_similar&utm_term=%E7%A1%AC%E4%BB%B6SPI+lcd&spm=1018.2226.3001.4187

 

https://blog.csdn.net/qq997758497/article/details/105347705?ops_request_misc=&request_id=&biz_id=102&utm_term=stm32%20spi%20lcd&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-9-.pc_search_similar&spm=1018.2226.3001.4187

五、      软件流程框图

主程序流程图:

FnKhD1hTS7wzhFVGZiu5L4k3UWoO

按键检测流程图:

FmHih_s_H_Qmb7JytWWWTn5iat5Z

模式切换流程图:

Fv7otvmoQb8YAtmSoxv1n6Fo_4AT

波形更新流程图:

FpXnRq9MRQr3u2jPXRW7U-pUH6v2

六、      主要部分设计思路

1.     硬件电路分析

FiB_QFzzEomUaf9dUmdyAEfwnGfw

STM32的输出PWM_WAVE口到WAV管脚为一个二阶有源滤波电路。其截止频率为100kHz,可以滤去不需要的波形成分。对于波形输出时,只需要分析其增益,此时可以做直流分析,将所用电容看做断路,即为基本的反相放大电路。列出节点电流方程:

FuadZhRbh04i23sq3tBUnxcDNroA

化简可得

FuE-fGtq1x3Njd30-Blym7nvqle1

理论上可以实现-4V~4V的输出

2.     波形输出

以正弦波输出为例

1. DAC输出方式配置

a. 利用DMA进行数据传输

要输出正弦波,实际上是控制DAC以v=sin(t)的正弦函数关系输出电压。DAC虽为“数模转换”,但实际上也只能输出离散的模拟电压,无法输出真实的连续正弦函数波。因此,只能通过输出离散的电压采样值来模拟输出正弦波。一个周期内输出的采样点越多,对正弦函数的还原越好。实际测试中发现,一个周期内输出200个点以上可以输出较好的波形。

b. 触发方式:定时器触发(本工程使用TIM3)

采用DMA输出+定时器触发,可以在每一次定时器计数溢出事件产生时自动改变DAC输出。配合DMA传输,可以实现定时该边输出值。

需要注意:配置定时器触发时,一定要在响应定时器的Parameter界面将Trigger Event Selection配置为Update Event,并在软件初始化时开启定时器。否则无法触发DAC输出。

c. 开启输出缓存

开启输出缓存时,可以增大输出阻抗,带负载能力增强,但根据参考手册DAC无法输出0V(一般最低为0.1Vref)。考虑到DAC输出后接了有源滤波电路,因此最开始没有开启输出缓存。但实际使用中,发现一直无法输出波形。查阅参考手册后发现DMA模式下需要开启该输出缓存。

2. 计算得到正弦波数据表(利用Matlab)

采用DMA+TIM+DAC输出波形时,需要一系列存储空间存储要输出的数字量,用定时器更新事件触发DMA将这一连续的存储空间逐个输出,得到波形。因此,需要一个基本的正弦波数据表,以此为基础输出不同频率、幅值、直流偏置的正弦波。

Matlab程序如下。使用了regexpprep函数来拼接,生成数据后直接复制到IDE中即可

clc;clear;
n = 240; %采样点数
hn = 120;
amplitude = 3.5; %幅值
x = zeros(1,n);
sin_wave = zeros(1,n);
tri_wave = zeros(1,n);
zig_wave = zeros(1,n);
x = linspace(0,n-1,n);
sin_wave = amplitude*sin(x./n.*2.*pi);

sin_value = round((sin_wave-4)./(-2.424)./3.3.*4096);
sin_string = num2str(sin_value); 
%输入需要为字符行向量
sin_string = regexprep(sin_string,'\s*',','); %拼接,中间用逗号隔开

x1 = linspace(1,hn,hn);
tri_wave(1:hn) = amplitude*(x1./hn);
tri_wave(hn+1:n) = amplitude*(1 - x1./hn);

tri_value = round((tri_wave-4)./(-2.424)./3.3.*4096);
tri_string = num2str(tri_value); 
%输入需要为字符行向量
tri_string = regexprep(tri_string,'\s*',','); %拼接,中间用逗号隔开

zig_wave = amplitude*(x./n);

zig_value = round((zig_wave-4)./(-2.424)./3.3.*4096);
zig_string = num2str(zig_value); 
%输入需要为字符行向量
zig_string = regexprep(zig_string,'\s*',','); %拼接,中间用逗号隔开

3. 改变定时器触发周期来实现调整频率

Fn9x0TMyf3_Jv5zZ5mLCWPrSTQZH

PSC为TIM3的预分频系数

ARR为TIM3自动重载值

POINTS为采样点个数

本设计中,设计采样点240个,固定PSC=1,因此每次修改TIM3 ARR寄存器的值就可以实现修改频率

tim3_period = LIMIT_MAX_MIN((uint32_t)(100000.0f/p_awg->Frequency.actual_value) - 1, 99999, 0);
TIM3->CNT = 0;
__HAL_TIM_SET_AUTORELOAD(&htim3, tim3_period);

4. 根据幅值和直流偏置调整输出数据

k = awg.Amplitude.actual_value / awg.max_output;
b = (4*k + awg.Offset.actual_value - 4.05f) / (-0.00195f);
for(i=0;i<SAMPLE_POINTS;i++)
{
output_array[i] = LIMIT_MAX_MIN((uint16_t)(origin_array[i] * k + b), DAC_MAX, DAC_MIN);
}

5. 配置DMA自动输出数据,生成波形

注意:每次修改频率/DAC输出数据时,需要先关闭DMA输出和TIM3,完成对寄存器和输出数据数组的修改后再打开。否则无法完成修改

 

3.     按键检测

采用读取IO口电平状态来获取按键按下状态。按键按下瞬间,可能出现电压抖动,造成IO口电平频繁切换。要准确读取按键信息,就需要消抖措施。常用的有硬件消抖和软件消抖,其中硬件消抖主要采用在按键两端并联电容,在按下瞬间通过充放电来平滑电压跳变曲线;但实验仪器的电路设计中没有硬件消抖电路。因此需要软件消抖。

       初步时,我采用之前常用的状态机来实现软件消抖。设置三种状态:

typedef enum
{
	KEY_CHECKING = 0,
	KEY_CONFIRMING,
	KEY_RELEASING
}KEY_STATE;

typedef struct
{
	GPIO_TypeDef* GPIO_Port;
	uint16_t GPIO_Pin;
	KEY_STATE state;
	volatile uint8_t  button_flag;
	volatile uint16_t hold_flag;
}Key_TypeDef;
/*
C++才可以用结构体的引用作为形参
如:void Key_Check(Key_TypeDef &key_state)
C语言不可以,只能指针传参
*/
void Key_Check(Key_TypeDef *key_state)
{
	switch(key_state->state)
	{
		case KEY_CHECKING:
		{
			if(HAL_GPIO_ReadPin(key_state->GPIO_Port, key_state->GPIO_Pin) == GPIO_PIN_SET)
		{
			key_state->state = KEY_CONFIRMING;
		}
		break;
		}
		
		case KEY_CONFIRMING:
		{
			if(HAL_GPIO_ReadPin(key_state->GPIO_Port, key_state->GPIO_Pin) == GPIO_PIN_SET)
			{
				key_state->button_flag ++;
				key_state->state = KEY_RELEASING;
			}
			else if(HAL_GPIO_ReadPin(key_state->GPIO_Port, key_state->GPIO_Pin) == GPIO_PIN_RESET)
			{
				key_state->state = KEY_CHECKING;
			}
			break;
		}
		
		case KEY_RELEASING:
		{
			if(HAL_GPIO_ReadPin(key_state->GPIO_Port, key_state->GPIO_Pin) == GPIO_PIN_RESET)
			{
				key_state->state = KEY_CHECKING;
			}
			else
			{
				key_state->state = KEY_RELEASING;
				key_state->hold_flag++;
			}
			break;
		}		
		default: break;
	}
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{		
	if(htim->Instance == TIM7)
	{
		Key_Check(&Key_1);
		Key_Check(&Key_2);
		Key_Check(&Key_R);
		Key_Check(&Key_O);
		Key_Check(&Key_L);
	}
}

主要原理为通过定时器定时每10ms进一次定时器中断,读取IO口电压。

       但实际应用过程中,发现了无法解决的离奇错误,同样的代码对于两个按钮式按键Key_1和Key_2,Key_2功能正常而Key_1无法识别,进行debug发现可以正常读取Key_1口的电平状态;同样的代码对于拨盘开关,拨向左边正常识别,拨向右边始终无法改变状态。怀疑是10ms的检测周期出现问题,改变定时器溢出周期后情况仍没有变化。

       最终,考虑到截止日期临近,不得已采用函数内延时来实现案件读取。主要流程如下

FmHih_s_H_Qmb7JytWWWTn5iat5Z

     

/**
  * @brief           Pass the current button state to the upper layer.
  * @note            Support debounce, short press(less than 1s) and long press(hold, more than 1s).
  * @param[in, out]  button_state  Encoded button state with long press flag added.
  *                    Bit5: Long press(hold) flag.
  * @retval          None
  */
void read_button(uint8_t *button_state)
{
	uint8_t i;
	if (*button_state != NO_KEY_PRESSED && encode_button())
	{
		*button_state = 0xFF;
		return ;
	}

	// Read button states.
	*button_state = encode_button();
	if (*button_state == NO_KEY_PRESSED)
		return ;  // None of the buttons has been pressed.

	HAL_Delay(50);
	*button_state = encode_button();
	if (*button_state == NO_KEY_PRESSED)
		return ;  // None of the buttons has been pressed.

	//1s以上算长按
	for (i = 0; i < 20; i++)
	{
		HAL_Delay(50);
		if (encode_button() == NO_KEY_PRESSED)  // The pressed button has been released.
			return ;
	}

	*button_state = 0x20 | encode_button();  // One or two buttons have been held, return the button state.
}

/**
  * @brief  Encode button state into a 5-bit integer.
  * @retval Encoded button state.
  * 		In every bit, 1 represents the corresponding button has been pressed,
			0 represents the corresponding button has not been pressed.
  *           Bit0: Button "OK"
  *           Bit1: Button "D"
  *           Bit2: Button "U"
  *           Bit3: Button "R"
  *           Bit4: Button "L"
  */
inline uint8_t encode_button(void)
{
	return (uint8_t)HAL_GPIO_ReadPin(Key_1_GPIO_Port, Key_1_Pin)<<4\
	| (uint8_t)HAL_GPIO_ReadPin(Key_2_GPIO_Port, Key_2_Pin) << 3 \
	| (uint8_t)HAL_GPIO_ReadPin(Key_R_GPIO_Port, Key_R_Pin) << 2 \
	| (uint8_t)HAL_GPIO_ReadPin(Key_O_GPIO_Port, Key_O_Pin) << 1 \
	| (uint8_t)HAL_GPIO_ReadPin(Key_L_GPIO_Port, Key_L_Pin);
}

检测函数在While循环中进行。检测到电平上升沿时,延时10ms后再次读取IO口电压,以确认是否按下。同时可以检测长按。

       这种按键检测方法也有其可取性。对于信号发生器,只有按键按下后才需要改变显示或输出状态。没有检测到按键时直接返回,接下来的数据处理也直接返回,可以灵敏地检测按键;按键按下时,也只有确认按键按下才会进一步启动update函数,流水执行。

4.     界面设计

界面分为两种显示:不可更改显示和可更改显示

对于不可更改显示部分,在初始化时就完成显示,只有在切换模式时才会改变

对于可更改显示的部分,采用面向对象的设计方法,经过分析,需要实现以下功能:

  1. 可通过按键模拟光标切换,实时显示选中的部分
  2. 输出有输出数字和输出字符两种类型。对于输出数字,可以通过拨盘改变选中的数字的大小;对于输出字符,可以通过拨盘改变显示的内容(改变单位)
  3. 显示数字部分,需要有最大值和最小值,并且能回环显示

因此,采用面向对象的设计思路,我设计了Cell_TypeDef结构体,统一定义这些可更改显示的部分

typedef struct
{
	uint16_t LCD_X;
	uint16_t LCD_Y;
	short*	 p_num;
	short		 num_min;
	short    num_max;
	char*		 text;
	uint8_t  len;
	uint8_t  cell_mode;		//为0表示输出数字,为1表示输出字符串
	uint8_t  is_selected; //为0表示该cell未被选中,为1表示被选中
}Cell_Typedef;

 

其中,LCD_X LCD_Y为该cell的初始显示坐标;cell_mode为cell显示模式标志,为0是显示数字,为1时显示字符;is_selected为cell选中标志,若cell未被选中,显示时为黑底白字;cell被选中时,显示翻转为白底黑字;p_num为short型指针,指向该cell显示的变量的地址。使用指针传值可以直接对该变量进行改变;text为char型指针,指向该cell显示的字符的首地址。

Cell类的成员函数有

void Cell_TFT_Show(Cell_Typedef *cell);			//显示
uint8_t Cell_getLength(Cell_Typedef* cell);	//获取数字/字符长度
void Cell_numIncrease(Cell_Typedef *cell);	//数字递增
void Cell_numDecrease(Cell_Typedef *cell);	//数字递减

 

通过地址传参,即可以直接对每个cell的结构成员作出修改。

5.     代码规范化管理

C++引入了类和命名空间,以及相应的继承、重载、多态等特性。而没有这些特性的C语言,在开发过程中经常因命名空间混乱,数据、函数被多文件调用而引发多种BUG。但C++的某些特性并不适合在单片机上实现,所以我选择继续使用C语言开发,通过更规范的代码管理来实现C++的优点。

typedef struct
{
	Value_TypeDef Frequency;			//默认1KHz  单位
	Value_TypeDef DutyCycle;		
}PWM_TypeDef;

typedef struct
{
	void (*cellInit)(PWM_TypeDef *p_pwm);
	void (*interfaceInit)(PWM_TypeDef *p_pwm);
	void (*dataInit)(PWM_TypeDef *p_pwm);

	void (*interfaceUpdate)(PWM_TypeDef *p_pwm);
	void (*updateOutput)(PWM_TypeDef *p_pwm);
}PWM_FuncSpace;

通过利用函数空间,统一管理PWM类的各个成员函数。C语言中,函数名即为指向该函数的指针。

const PWM_FuncSpace PWM = {
	.cellInit = PWM_cellInit,
	.interfaceInit = PWM_interfaceInit,
	.dataInit = PWM_dataInit,
	.interfaceUpdate = PWM_interfaceUpdate,
	.updateOutput = PWM_updateOutput
};

初始化时统一分配即可。调用时,使用PWM.cellInit(& pwm);调用即可。避免了命名空间的混乱。

七、      结果展示

界面(函数发生器):

FuDGXckAH590qKML-xvcrU1Vg-_L

FuctCvbEanzwG6Veoq8303cwcCGj

正弦函数输出

Fvn3h5OUK3Xz7ugptx0RbFrb8jba

三角波输出

FogkuU5l3JLRRQRXh-P_KygN231N

锯齿波输出

FuZXHlH_Jg0J2F3uyo6ZD6Plruu4

PWM波输出

FvXEABUHjFQQvrCLFfJ0436izBBp

八、      总结与展望  

本次暑期一起学,锻炼了我独立开发单片机应用程序的能力,尤其是开发过程中独立分析问题,指定研发方案,不断优化的能力。同时,也实现了以“面向对象”的思想来开发C程序,代码管理方面也有了更多新尝试。总之收获满满。

但仍存在以下不足:

  1. LCD屏幕使用硬件SPI,但没有使用DMA(与DAC的DMA通道冲突),导致刷新较慢
  2. LCD的显示驱动函数也有优化空间。切换模式时明显比实例程序慢不少

一点小建议:

   之后的暑期一起学项目,STM32相关的,希望刘勇SWD调试接口。否则无法DEBUG将导致开发格外困难。(这一次是留了焊盘,我自己焊了线上去才得以DEBUG)。不DEBUG着实很难分析问题。

 

附件下载
F072_TFT.axf
可直接下载的16进制文件
AWG.7z
工程文件
团队介绍
Old_ChoBo的暑期solo 华中科技大学光学与电子信息学院
团队成员
Old_ChoBo
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号