基于STM32+iCE40的电赛训练平台实现DDS任意波形发生器/本地控制
使用FPGA+MCU架构完成任意波形发生器,DAC 并行数模转换芯片速度比较高 ,数据发送用FPGA做比较合理;而OLED显示、按键、旋转编码器的控制使用用MCU处理较为简单;发挥两者的优势,高效简捷地实现任意波形发生器。
标签
嵌入式系统
FPGA
DDS
且听风吟123
更新2023-03-28
1679

一、项目具体要求

  1. 通过板上的高速DAC(10bits/125Msps)配合FPGA内部DDS的逻辑,生成波形可调(正弦波、三角波、方波)、频率可调、幅度可调的波形
  2. 生成模拟信号的频率范围为DC-5MHz,调节精度为1Hz
  3. 生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V
  4. 通过UART同PC连接,在PC上可以使用Matlab、Labview或其它调试工具来控制波形的切换、参数的改变

二、设计思路

        通过MCU、FPGA外设(旋转编码器、按键)调整波形参数(波形、频率、幅值),单片机将波形频率控制字通过SPI总线发送给FPGA,同时将波形频率及频率控制字显示在OLED屏幕上。FPGA根据MCU发送的数据以及外部按键的输入,结合DDS原理,控制DAC并行数模转换芯片,从而输出符合要求的波形。

FpvQ0WXeVoTO6cVJmyN_HRorOxVr

三、开发工具配置

1、FPGA开发工具使用及下载

使用LATTICE  Radiant开发FPGA程序,需要注意工程路径不允许存在中文。

(1)综合分析

FtxmZb3hBf9FcOb5oFvHcTR58uey

(2)分配管脚

Fuwj6Z_Y8UDBFPf1oF1BwgiV4Q7j

(3)设置输出文件格式为rbt

FsJYKBH8HMeAsXUD1f_jDt30zqzGFjO5ONSSxj4RdBN7mSruCsn2HVzE

(4)下载方式

FlWN5DoPB1QKcEmfI6-sH61FTBBv

 

2、MCU开发工具STM32CubeMX

FpboirsStYgMzApf8G72aXiGbjEXFpL77uqvQjIbVCFLIIUZO7IU4ZYLFms-SQJAwgK1xa3O5XB5Nmg97HqP

 

3、MCU下载工具STM32CubeProgrammer

按住BOOT0(将此管脚拉高),再上电,连接USARTFrVpdveSI6PG9xnh9Oq35aOp820b

打开HEX文件,下载

FiNzDTEJNn9qa4-waWIrjHKIepWH

四、程序实现

1、MCU功能:OLED显示、按键及旋转编码器检测、串口通信、SPI通信

(1)OLED初始化

//OLED初始化
void OLED_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStruct = {0};
  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOA_CLK_ENABLE();
  /*Configure GPIO pin : oled_Pin */
  GPIO_InitStruct.Pin = SCL_Pin|SDA_Pin|RES_Pin|DC_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
  HAL_GPIO_Init(SCL_GPIO_Port, &GPIO_InitStruct);
	HAL_GPIO_WritePin(SCL_GPIO_Port, SCL_Pin|SDA_Pin|RES_Pin|DC_Pin, GPIO_PIN_SET);
	
	OLED_RES_Clr();
	HAL_Delay(200);
	OLED_RES_Set();
	
	OLED_WR_Byte(0xAE,OLED_CMD);//--turn off oled panel
	OLED_WR_Byte(0x00,OLED_CMD);//---set low column address
	OLED_WR_Byte(0x10,OLED_CMD);//---set high column address
	OLED_WR_Byte(0x40,OLED_CMD);//--set start line address  Set Mapping RAM Display Start Line (0x00~0x3F)
	OLED_WR_Byte(0x81,OLED_CMD);//--set contrast control register
	OLED_WR_Byte(0xCF,OLED_CMD);// Set SEG Output Current Brightness
	OLED_WR_Byte(0xA1,OLED_CMD);//--Set SEG/Column Mapping     0xa0左右反置 0xa1正常
	OLED_WR_Byte(0xC8,OLED_CMD);//Set COM/Row Scan Direction   0xc0上下反置 0xc8正常
	OLED_WR_Byte(0xA6,OLED_CMD);//--set normal display
	OLED_WR_Byte(0xA8,OLED_CMD);//--set multiplex ratio(1 to 64)
	OLED_WR_Byte(0x3f,OLED_CMD);//--1/64 duty
	OLED_WR_Byte(0xD3,OLED_CMD);//-set display offset	Shift Mapping RAM Counter (0x00~0x3F)
	OLED_WR_Byte(0x00,OLED_CMD);//-not offset
	OLED_WR_Byte(0xd5,OLED_CMD);//--set display clock divide ratio/oscillator frequency
	OLED_WR_Byte(0x80,OLED_CMD);//--set divide ratio, Set Clock as 100 Frames/Sec
	OLED_WR_Byte(0xD9,OLED_CMD);//--set pre-charge period
	OLED_WR_Byte(0xF1,OLED_CMD);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
	OLED_WR_Byte(0xDA,OLED_CMD);//--set com pins hardware configuration
	OLED_WR_Byte(0x12,OLED_CMD);
	OLED_WR_Byte(0xDB,OLED_CMD);//--set vcomh
	OLED_WR_Byte(0x40,OLED_CMD);//Set VCOM Deselect Level
	OLED_WR_Byte(0x20,OLED_CMD);//-Set Page Addressing Mode (0x00/0x01/0x02)
	OLED_WR_Byte(0x02,OLED_CMD);//
	OLED_WR_Byte(0x8D,OLED_CMD);//--set Charge Pump enable/disable
	OLED_WR_Byte(0x14,OLED_CMD);//--set(0x10) disable
	OLED_WR_Byte(0xA4,OLED_CMD);// Disable Entire Display On (0xa4/0xa5)
	OLED_WR_Byte(0xA6,OLED_CMD);// Disable Inverse Display On (0xa6/a7) 
	OLED_WR_Byte(0xAF,OLED_CMD);
	OLED_Clear();
	OLED_WR_Byte(0xAF,OLED_CMD);
}

(2)外部中断初始化EXTI_Init,设置中断线优先级

		//中断线0-PB0
    HAL_NVIC_SetPriority(EXTI0_1_IRQn,1,0);            //抢占优先级为1,子优先级为0
    HAL_NVIC_EnableIRQ(EXTI0_1_IRQn);                  //使能中断线2
    
    //中断线11-PA11
    HAL_NVIC_SetPriority(EXTI4_15_IRQn,2,0);           //抢占优先级为2,子优先级为0
    HAL_NVIC_EnableIRQ(EXTI4_15_IRQn);                 //使能中断线2

HAL库中可以通过HAL_NVIC_SetPriority函数来设置中断的优先级,决定中断是否能够被抢占。第一个参数为要设置的中断号,第二个参数为抢占优先级,有0~3个四个等级,值越小表示的优先级越高,即抢占优先级0的中断的优先级高于抢占优先级1的中断。第三个参数为响应优先级,从底层代码注释可以看出对于Cortex M0+的产品不支持该参数,该参数不用设置。

void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(SubPriority);

  /* Check the parameters */
  assert_param(IS_NVIC_PREEMPTION_PRIORITY(PreemptPriority));
  NVIC_SetPriority(IRQn, PreemptPriority);
}

中断服务函数EXTI0_1_IRQHandler处理PB0的外部中断

中断服务函数EXTI4_15_IRQHandler处理PA11、PA12、PC6的外部中断

中断服务函数会调用HAL_GPIO_EXTI_IRQHandler,在其中处理上升沿中断和下降沿中断

在上升沿中断和下降沿中断中检测按键及旋转编码器,修改波形参数并将同步更新到OLED

void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
{
	switch(GPIO_Pin)
	{
		case key1_Pin:                                                 //复位按键
		{
			freq = 0; //正弦波频率
			freq_step = 0; //正弦波调节步进,送到fpga
			step_flag = 0; //正弦波步进标志,为0步进10Hz,为1步进100Hz
			OLED_Clear1();
			SetCursor(40,16);
			printf("%.1fkHz",freq/1000.0);
			SetCursor(40,32);
			printf("1Hz ");
			//SetCursor(0,48);
			//printf("%10d",freq_step);
			OLED_Refresh();
			break;
		}
		case key2_Pin:                                                 //切换频率步进按键
		{
			switch(step_flag)
			{
				case 0: step_flag = 1; //10Hz步进
								SetCursor(40,32);
								printf("10Hz  ");
								break;
				case 1: step_flag = 2; //100Hz步进
								SetCursor(40,32);
								printf("100Hz  ");
								break;
				case 2:	step_flag = 3; //1kHz步进
								SetCursor(40,32);
								printf("1kHz  ");
								break;
				case 3:	step_flag = 4; //10kHz步进
								SetCursor(40,32);
								printf("10kHz ");
								break;
				case 4:	step_flag = 5; //100kHz步进
								SetCursor(40,32);
								printf("100kHz");
								break;
				case 5:	step_flag = 6; //1MHz步进
								SetCursor(40,32);
								printf("1MHz  ");
								break;
				case 6:	step_flag = 0; //10Hz步进
								SetCursor(40,32);
								printf("1Hz ");
								break;
				default:	step_flag = 0; //10Hz步进
									SetCursor(40,32);
									printf("1Hz ");
									break;
			}
			OLED_Refresh();
			break;
		}
		case encoderA_Pin:                                             //编码器旋钮,调节频率
		{
			if(HAL_GPIO_ReadPin(encoder_GPIO_Port,encoderB_Pin)) //A下降沿,B高电平,顺时针
			{
				switch(step_flag)
				{
					case 0: freq += 1;	break;
					case 1: freq += 10;	break;
					case 2: freq += 100;	break;
					case 3: freq += 1000;	break;
					case 4: freq += 10000;	break;
					case 5: freq += 100000;	break;
					case 6: freq += 1000000;	break;
					default: freq += 1;  break;
				}
				if(freq>10000000) freq=10000000;
				freq_step = freq*71.5827883;
			}
			else if(!HAL_GPIO_ReadPin(encoder_GPIO_Port,encoderB_Pin)) //A下降沿,B低电平,逆时针
			{
				switch(step_flag)
				{
					case 0: freq -= 1;	break;
					case 1: freq -= 10;	break;
					case 2: freq -= 100;	break;
					case 3: freq -= 1000;	break;
					case 4: freq -= 10000;	break;
					case 5: freq -= 100000;	break;
					case 6: freq -= 1000000;	break;
					default: freq -= 1;  break;
				}
				if(freq>10000000) freq=0;
				freq_step = freq*71.5827883;
			}
			OLED_Clear1();
			SetCursor(40,16);
			if(freq<1000000)
			{
				printf("%.3fkHz",freq/1000.0);
			}
			else
			{
				printf("%.6fMHz",freq/1000000.0);
			}
			//SetCursor(0,48);
			//printf("%10d",freq_step);
		  //OLED_Refresh();

			break;
		}
		case encoder_key_Pin:                                             //编码器按键,改变频率
		{
			//显示发送给FPGA的频率
			SetCursor(0,48);
			printf("%10d",freq_step);
			OLED_Refresh();
			
			spi_trans = freq_step>>24;
			HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
			spi_trans = freq_step>>16;
			HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
			spi_trans = freq_step>>8;
			HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
			spi_trans = freq_step;
			HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
			break;
		}
		default:	break;
	}
}

(3)串口通信

通过STM32CubeMx配置串口通信功能

FgzwYLsSRS1p7Kp7M-yhBaJwacEO

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart ->Instance == USART2)
    {
				if((USART2_RX_STA&0x8000)==0)
				{
					if(Res==0x0D)
					{
						USART2_RX_STA|=0x8000;
						HAL_UART_Receive_IT(&huart2, &Res, 1);
					}
					else
					{
						USART2_RX_BUF[USART2_RX_STA&0X3FFF]=Res ;
						USART2_RX_STA++;
						if(USART2_RX_STA>(USART_REC_LEN-1))
							USART2_RX_STA=0;
					}		 
				}
				HAL_UART_Receive_IT(huart,&Res,1);
    }
}

(4)SPI通信

通过STM32CubeMx配置SPI通信功能

SCLK: 串行时钟(由主设备输出).

MOSI: 主输出、从输入(由主设备输出).

MISO: 主输入、从输出(由从设备输出).

NSS: 从设备选中(低有效, 由主设备输出).

Fkwy2vTzlqyFFHJ74Vb-ZoS6cdyt

spi_trans = freq_step>>24;
			HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
			spi_trans = freq_step>>16;
			HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
			spi_trans = freq_step>>8;
			HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);
			spi_trans = freq_step;
			HAL_SPI_Transmit(&hspi1,&spi_trans,1,1000);

2、FPGA核心功能:DDS波形发生器

将外部12MHz晶振5倍频后作为系统时钟

(1)频率控制

dds_training_block.png?cache=

通过相位累加器实现频率的控制,系统时钟为60MHz,选取相位累加器位数为32位,根据公式可以计算得到1Hz时,频率控制字为232/(60×106)≈71.58。由此可知,为了实现1Hz的控制精度,频率控制字的步进值为71.58。

正弦波通过查表法实现,使用的rom是宽度为10,深度为1024的数据,所以相位控制字根据rom的深度选择了10位宽。根据相位累加器的高10位,进行波形查找。同理,方波、三角波可以利用相位累加器实现频率的控制。具体代码如下

//0:方波
wire cnt_tap = phase_acc[31];             // 取出计数器的其中1位(bit 7 = 第8位)
assign square_dac = {10{cnt_tap}};        // 重复10次作为10位DAC的值 
//1:三角波
assign trig_dac = phase_acc[31] ? ~phase_acc[30:21] : phase_acc[30:21];
//2:正弦波
assign sin_dac = rd_data_i; //将读到的ROM数据赋值给DA数据端口

(2)波形控制

方波:

module dds_main(clk, dac_data, dac_clk);
input clk;                     //12MHz的外部时钟送给FPGA;
output [9:0] dac_data;            //10位的并行数据输出到R-2R DAC; 
output dac_clk;                   //输出给DAC的并行时钟,在R-2R的DAC中不需要这个时钟信号
 
//创建一个24位的自由运行的二进制计数器,方便取出最高位(0.7秒一个周期)送给LED用作心跳灯指示用, 在此程序中不列出LED的部分
//后面也会讲到为什么DDS波表的地址只有8~12位,而我们选用24位或32位相位累加器的原因
reg [23:0] cnt;
always @(posedge clk) cnt <= cnt + 24'h1;
 
//用它来产生DAC的信号输出
wire cnt_tap = cnt[7];             // 取出计数器的其中1位(bit 7 = 第8位)
assign dac_data = {10{cnt_tap}};   // 重复10次作为10位DAC的值 
assign dac_clk= clk; 
 
endmodule

三角波

assign dac_data = cnt[10] ? ~cnt[9:0] : cnt[9:0];

(3)幅值控制

信号发生器通常采用“DAC参考电压”配合“模拟通道信号调理”进行信号幅值的调节,市面上大多数信号发生器产品都是采用这种调幅方案。这样在调节的过程中信号的分辨率不会受影响,最大程度保证信号的性能指标,同时也需要额外的DAC等电路控制参考电压。

另外我们也可以在FPGA中增加对信号幅值调节的设计,例如我们将信号的数据乘以一个8bit的因数,然后再将结果右移8位(相当于除以256),然后再把结果输出给DAC电路。将8bit的因数作为变量可调,最后就实现了在FPGA中调节幅值的功能。这种方法不需要额外的DAC改变参考电压,即可实现对幅值的调节,但是由于采用量化后的数据进行,会造成信号数据分辨率的降低。

wire [9:0] signal_dat; //未调幅的波形数据
wire [7:0] a_ver; //用于调幅的因数
 
reg [17:0] amp_dat; //调幅后的波形数据
always @(posedge clk) amp_dat = signal_dat * a_ver;  //波形数据乘以调幅因数
 
wire [9:0] dac_dat; //输出给DAC电路的数据端口
assign dac_dat = amp_dat[17:8]; //取高十位输出,相当于右移8位

根据上述DDS波形发生器原理,结合自己的业务逻辑(接收单片机发送的频率字、通过按键控制波形、幅值),输出对应的波形,具体代码如下

module dds_output(
	input	clk_i,	//时钟
	input	rst_n_i, //复位信号,低电平有效
	input   [1:0] mode,    //波形
	input   [7:0] amp,     //幅值
	
	input   [31:0] freq_set, //频率设置
	
	input	[9:0]	rd_data_i, //ROM读出的数据
	
	output reg led11,
	output reg led22,

	output	[9:0]	rd_addr_o, //读ROM地址
	//DA芯片接口
	output	da_clk_o, //DA驱动时钟
	output	[9:0]	da_data_o //输出给DA的数据
	
	);
	

//reg define
reg [31:0] phase_acc;   //相位累加器


wire [9:0] square_dac;
wire [9:0] trig_dac;
wire [9:0] sin_dac;

reg [9:0] signal_dat; //未调幅的波形数据
reg [17:0] amp_dat; //调幅后的波形数据

//*****************************************************
//** main code
//*****************************************************

//数据rd_data是在clk的上升沿更新的,所以DA芯片在clk的下降沿锁存数据是稳定的时刻
//而DA实际上在da_clk的上升沿锁存数据,所以时钟取反,这样clk的下降沿相当于da_clk的上升沿
assign da_clk_o = ~clk_i;
//0:方波
wire cnt_tap = phase_acc[31];             // 取出计数器的其中1位(bit 7 = 第8位)
assign square_dac = {10{cnt_tap}};        // 重复10次作为10位DAC的值 
//1:三角波
assign trig_dac = phase_acc[31] ? ~phase_acc[30:21] : phase_acc[30:21];
//2:正弦波
assign sin_dac = rd_data_i; //将读到的ROM数据赋值给DA数据端口

//显示波形、幅值的变化状态
always @(posedge clk_i) begin
	if(mode%2==0)
		led11=1'b0;
	else
		led11=1'b1;
		
	if(amp%20==0)
		led22=1'b0;
	else
		led22=1'b1;
end
	

//相位累加器累加
//波形选择+幅值调整
always @(posedge clk_i or negedge rst_n_i) begin
	if(rst_n_i == 1'b0) begin
		phase_acc <= 32'b0;
	end
	else begin
		phase_acc <= phase_acc + freq_set;
		
		case(mode)
			2'b00: amp_dat = trig_dac * amp;
			2'b01: amp_dat = sin_dac * amp;
			2'b10: amp_dat = square_dac * amp;
			2'b11: amp_dat = 18'd0;
		endcase
		
	end
end


//读ROM地址
assign rd_addr_o = phase_acc[31:22];

assign da_data_o = amp_dat[17:8]; //取高十位输出,相当于右移8位
	
endmodule

五、遇到的难题

1、不同尺寸OLED驱动代码不同,需要移植对应驱动代码,在这一方面走了一些弯路

2、不同系列单片机提供外部中断服务函数可能不同

STM32G0包括如下三个外部中断服务函数

EXTI0_1_IRQn         EXTI0_1_IRQHandler

EXTI2_3_IRQn         EXTI2_3_IRQHandler

EXTI4_15_IRQn       EXTI4_15_IRQHandler

即外部中断线0-1分配一个中断向量,共用一个中断服务函数

外部中断线2-3分配一个中断向量,共用一个中断服务函数

外部中断线4-15分配一个中断向量,共用一个中断服务函数

因此EXTI4_15_IRQHandler可以处理PA11、PA12、PC6的下降中断

3、以前都是使用标准库开发MCU程序,第一次使用HAL进行开发,很多函数都是现查现用;

同时也是第一次接触FPGA,通过这次项目也算正式入门了。

4、首次使用接触FPGA,在学习Verilog语言的过程。了解到模块的输入、输出必须是wire类型

可是根据按键上升沿改变值必须是reg类型,之前一直在这个矛盾中循环,其实只需要在模块内部定义一个reg类型的临时变量,再通过assign语句将其赋值给输出的wire变量。

5、SPI通信速率

按说可以采样到18Mhz的数据,但是,当STM32的SPI设置为18Mhz时会出错,接收到的数据总会出错。降低通信速率,顺利解决该问题。

六、未来的计划

已经初步实现通过串口控制波形频率,下一步完善MCU与FPGA之间的SPI通信,实现波形、幅值信息的传递,进而实现DDS任意波形发生器/PC远程控制。此外,利用开发板进一步实现单通道示波器的功能,不断提升自己。

七、工程文件

链接:https://pan.baidu.com/s/1s2bHOufqqirEKNofpeHNjg 
提取码:3cda

附件下载
可以直接烧录的程序.rar
团队介绍
测控工程师一枚
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号