基于STM32+iCE40电赛训练平台的DDS任意波形发生器(本地控制)
板卡主要包括单片机部分和FPGA部分。单片机部分主要用于显示、控制和复杂运算;FPGA主要用于数据的运输和处理。两者以SPI为桥梁进行通信。最终完成了硬禾所要求的《DDS任意波形发生器/本地控制》的要求。
标签
嵌入式系统
FPGA
数字逻辑
DDS
zhiwu
更新2023-03-28
南京邮电大学
663

一、项目需求

1、通过板上的高速DAC(10bits/最高125Msps)配合FPGA内部DDS的逻辑(最高48Msps),生成波形可调(正弦波、三角波、方波)、频率可调(DC-)、幅度可调的波形

2、生成模拟信号的频率范围为DC-5MHz,调节精度为1Hz

3、生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V

4、在OLED上显示当前波形的形状、波形的频率以及幅度

5、利用板上旋转编码器和按键能够对波形进行切换、进行参数调节

二、完成的功能及达到的性能

1、选择模式

可选择波形选择、幅度控制、频率控制三种模式。在板卡上通过mode按键选择,按下后光标自动跳转到下一个模式,默认处于波形选择模式。

2、选择波形

在波形选择模式下,可以选择正弦波、三角波、方波三种波形,每按下一次select按键跳转到下一个波形,默认为正弦波

3、幅度控制

在幅度控制模式下,我们可以调节幅度大小和选择单位步进幅度大小。首先选择调节幅度步进大小,当步进0.1V时,OLED屏幕上会显示mv单位;当步进1V时,OLED屏幕上显示的是V单位。通过旋转编码器来控制数值大小,顺时针旋转时幅值增大,反之减小。

4、频率控制

控制方法与幅度控制相同,这里将单位改为hz、khz与Mhz,步进大小改为1hz、1khz和1Mhz。

5、输入输出

按下旋转编码器按键即可选择输入输出状态,状态会在屏幕上显示。FvikGJ6mCWwd2KgOq0AU87tlu3pEFqpnhTn_wqhrseC1S_J4FRG9zVLb

三、实现思路

板卡主要分为单片机部分和FPGA部分。

1、单片机部分

单片机主控芯片为stm32g031g8u6,是一款性价比较高的芯片。它主要负责的功能有外界交互、OLED显示、复杂运算和充当与FPGA通信的主机。

2、FPGA部分

主要负责DDS波形生成和充当通信的从机。能够对单片机的指令进行翻译并进行反馈。

Fsy2y46-oT0nghEfnZASXel827YU

四、实现过程

1、按键中断

对于按键消抖,我采用了定时器消抖的方法,这样的方法可以避免中断长时间占用cpu,达到快进快出的目的。当按键触发了中断回调函数,我便开启定时器中断,产生定时器中断后再次检测按键电平。

//定时器状态机消抖
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if(htim->Instance == TIM14)
	  {
	    switch(Key.keyState)
	    {
	      case KEY_CHECK:
	      {
	        // 读到低电平,进入按键确认状态
	        if((HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_11) ==  GPIO_PIN_RESET)||(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_12) ==  GPIO_PIN_RESET) || 
			(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_6) ==  GPIO_PIN_SET))
	        {
	          Key.keyState = KEY_COMFIRM;
	        }
	        break;
	      }
	      case KEY_COMFIRM:
	      {
	        if((HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_11) ==  GPIO_PIN_RESET)||(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_12) ==  GPIO_PIN_RESET) || 
			(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_6) ==  GPIO_PIN_SET))
	        {
	          //读到低电平,按键确实按下,按键标志位置1,并进入按键释放状态
	          Key.keyFlag = 1;
	          Key.keyState = KEY_RELEASE;

	        }
	          //读到高电平,可能是干扰信号,返回初始状态
	        else
	        {
	          Key.keyState = KEY_CHECK;
	        }
	        break;
	      }
	      case KEY_RELEASE:
	      {
	        if((HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_11) ==  GPIO_PIN_RESET)||(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_12) ==  GPIO_PIN_RESET) || 
			(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_6) ==  GPIO_PIN_SET))
	         {
	           // 读到高电平,说明按键释放,返回初始状态
	           Key.keyState = KEY_CHECK;
			   HAL_TIM_Base_Stop_IT(&htim14);//关闭中断,下一次按键触发后开启
	         }
	         break;
	      }
	      default: break;
	    }
	  }
}

编码器部分设置A和B引脚都由下降沿触发中断,此时可根据旋转方向分为两种情况。A引脚触发中断,此时若B引脚为高电平,则是一个方向的旋转周期,此时cnt改变;B引脚触发中断,此时A引脚若为高电平,则是相反方向的旋转,此时cnt按照相反方向改变。

//由两个按键和旋转编码器引脚引起的中断
void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
{
	HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_8);
	//旋转编码器引脚
	if(GPIO_Pin == GPIO_PIN_0)
	{
		if((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) ==  GPIO_PIN_RESET))
		{
			if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) ==  GPIO_PIN_SET)
			{
				cnt++;
			}
		}
	}
	else if(GPIO_Pin == GPIO_PIN_1)
	{
		if((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) ==  GPIO_PIN_RESET))
		{
			if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) ==  GPIO_PIN_SET)
			{
				cnt--;
			}
		}
	}
	//模式和选择按键下降沿触发
	else
	{
		HAL_TIM_Base_Start_IT(&htim14);//打开定时器进行消抖
	}
}

除状态输出按键外均设置为下降沿触发

  /*Configure GPIO pins : PB0 PB1 */
  GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
  /*Configure GPIO pins : PA11 PA12 */
  GPIO_InitStruct.Pin = GPIO_PIN_11|GPIO_PIN_12;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  /*Configure GPIO pin : PC6 */
  GPIO_InitStruct.Pin = GPIO_PIN_6;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
  /* EXTI interrupt init*/
  HAL_NVIC_SetPriority(EXTI0_1_IRQn, 1, 0);
  HAL_NVIC_EnableIRQ(EXTI0_1_IRQn);
  HAL_NVIC_SetPriority(EXTI4_15_IRQn, 1, 0);
  HAL_NVIC_EnableIRQ(EXTI4_15_IRQn);

2、OLED屏幕

SSD1306是屏幕的驱动芯片,任何屏幕都需要驱动芯片。这里因为有驱动芯片了,我们不需要研究屏幕的驱动电路,只需与SSD1306芯片通信即可。我移植了u8g2库,由于stm32g031g8u6仅有64kflash,而绘制显示需要微控制提供一定的内存。故需要对特别注意以下两个函数。若将构造函数(第一个)的末尾的数字改成f,单片机的flash内存将无法支持。

void u8g2_Setup_ssd1306_128x64_noname_2(u8g2_t *u8g2, const u8g2_cb_t *rotation, u8x8_msg_cb byte_cb, u8x8_msg_cb gpio_and_delay_cb);
uint8_t u8x8_byte_3wire_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int,void *arg_ptr) ;

3、DDS波形生成

通过matlab生成三种波形的数据供读取,我们设置波形rom表的depth为8,最大值为2^10-1,那么就可以存储2^8个0~2^10-1的数据。这里路径需要改一下,改成波表文件所在路径即可。

reg [9:0] sine[0:255];//申请256个10位的存储单元
reg [9:0] square[0:255];//申请256个10位的存储单元
reg [9:0] sanjiao[0:255];//申请256个10位的存储单元

initial
	begin
		$readmemh("D:/FPGA/ICE40/DDS_fpga/source/impl_1/sine.txt",sine); //sine.txt中的数字到memory
		$readmemh("D:/FPGA/ICE40/DDS_fpga/source/impl_1/sanjiao.txt",sanjiao); 
		$readmemh("D:/FPGA/ICE40/DDS_fpga/source/impl_1/square.txt",square); 
	end

根据题目要求,需要实现固定幅度和频率的步进。对于频率的步进我采用相位累加器的方法。相位累加器本质上是一个计数器,这里起一个形象的名字phase_acc。对于幅度的控制,我将幅度先乘以调幅因子,再进行移位,可实现高精度的除法。

//生成波形
reg [41:0] phase_acc;
always @(posedge clk or negedge rst) begin
	if(!rst)
		phase_acc<=0;
	else
		phase_acc <= phase_acc + frezi[37:0];
end

always@(posedge clk or negedge rst)	
    if(!rst)
        sin_out<=0;
    else if(wave_state == 0)
        sin_out<=sine[phase_acc[41:34]]*vppzi;
    else if(wave_state == 1)
        sin_out<=sanjiao[phase_acc[41:34]]*vppzi;
    else
        sin_out<=square[phase_acc[41:34]]*vppzi;
        
always @(posedge clk or negedge rst) begin
	if(!rst)
		waveout<=0; 
	else if(enable)
		waveout<=sin_out[25:16];
	else
		waveout<=0;
end

4、SPI通信

对于SPI的通信原理不多做阐述,网上有很多很好的资料。NSS作用:用来选择主从设备。SPI_NSS有两种模式,SPI_NSS_Hard和SPI_NSS_Soft。SPI_NSS_Hard,硬件自动拉高拉低片选,在速率上是远比软件方式控制要高的。我设置FPGA为从机,单片机为主机。所有的指令由单片机发起,FPGA翻译。

单片机代码

设置为主机模式,SPI_NSS_HARD模式,极性为低,相位为1,分频为256,MSB优先。

/* SPI1 init function */
void MX_SPI1_Init(void)
{

  /* USER CODE BEGIN SPI1_Init 0 */

  /* USER CODE END SPI1_Init 0 */

  /* USER CODE BEGIN SPI1_Init 1 */

  /* USER CODE END SPI1_Init 1 */
  hspi1.Instance = SPI1;
  hspi1.Init.Mode = SPI_MODE_MASTER;
  hspi1.Init.Direction = SPI_DIRECTION_2LINES;
  hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
  hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
  hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
  hspi1.Init.NSS = SPI_NSS_HARD_OUTPUT;
  hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;
  hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
  hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
  hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
  hspi1.Init.CRCPolynomial = 7;
  hspi1.Init.CRCLength = SPI_CRC_LENGTH_DATASIZE;
  hspi1.Init.NSSPMode = SPI_NSS_PULSE_ENABLE;
  if (HAL_SPI_Init(&hspi1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN SPI1_Init 2 */

  /* USER CODE END SPI1_Init 2 */

}

FPGA代码

按照配合单片机时序移植的时序,这一块的代码移植的以下文章【原创】详细解析FPGA与STM32的SPI通信(二) -LinCoding-电子技术应用-AET-中国科技核心期刊-最丰富的电子设计资源平台 (chinaaet.com)

module spi_receiver
(
	input				clk,		//全局时钟信号
	input				rst_n,		//低电平有效复位
			
	input				spi_cs,		//片选信号
	input				spi_sck,	//spi时钟信号
	input				spi_mosi,	//主机输出从机接收线
	
	output	reg	[7:0]	rxd_data,   //接受数据
	output	reg			rxd_flag	
);

//-----------------------------------
//对异步时钟域信号进行处理,得到同步信号
reg	spi_cs_r0,	spi_cs_r1;
reg	spi_sck_r0,	spi_sck_r1;
reg	spi_mosi_r0,spi_mosi_r1;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		begin
			spi_cs_r0	<= 1'b1;		spi_cs_r1	<= 1'b1;
            spi_sck_r0	<= 1'b0;		spi_sck_r1	<= 1'b0;
            spi_mosi_r0	<= 1'b0;		spi_mosi_r1	<= 1'b0;
		end
	else
		begin
			spi_cs_r0	<= spi_cs;		spi_cs_r1	<= spi_cs_r0;
            spi_sck_r0	<= spi_sck;		spi_sck_r1	<= spi_sck_r0;
            spi_mosi_r0	<= spi_mosi;	spi_mosi_r1	<= spi_mosi_r0;
		end
end
reg	[3:0]	rxd_cnt /*synthesis noprune*/; // 避免Quartus II优化掉没output的reg,这个在Radiant里不知道效果如何,待实验
wire	mcu_cs	= spi_cs_r1;
wire	mcu_data= spi_mosi_r1;
wire	mcu_read_flag = ( spi_sck_r0 & ~spi_sck_r1) ? 1'b1 : 1'b0;	//捕捉spi主机时钟信号上升沿
wire	mcu_read_done = ( spi_cs_r0 & ~spi_cs_r1 & (rxd_cnt == 4'd8) ) ? 1'b1 : 1'b0;	//接受信号完成标志

//-----------------------------------
//MOSI线信号采样
reg		[7:0]	rxd_data_r;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		begin
			rxd_cnt		<= 4'd0;
			rxd_data_r	<= 8'd0;
		end
	else if ( ! mcu_cs )
		if ( mcu_read_flag )
			begin
				rxd_data_r[3'd7-rxd_cnt]	<= mcu_data;	//32单片机设置的是MSB,先接收最高位
				rxd_cnt				<= rxd_cnt + 1'b1;
			end
		else
			begin
				rxd_data_r			<= rxd_data_r;
				rxd_cnt				<= rxd_cnt;
			end
	else
		begin
			rxd_data_r	<= rxd_data_r;
			rxd_cnt		<= 4'd0;
		end
end

//-----------------------------------
//输出信号
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		begin
			rxd_data	<= 8'd0;
			rxd_flag	<= 1'b0;
		end
	else if ( mcu_read_done )
		begin
			rxd_data	<= rxd_data_r;
			rxd_flag	<= 1'b1;
		end
	else
		begin
			rxd_data	<= rxd_data;
			rxd_flag	<= 1'b0;
		end
end

endmodule

5、FPGA状态机翻译数据

这里我写得比较粗糙,好在做dds对数据的传输速率要求不高。我的大致思路是先接收到单片机发送的0和1,这样的话FPGA下一个八位接受到的就是波形的选择数据;单片机再发送2和3,FPGA下一个接收的是输出状态;单片机再发送4和5,FPGA下八次接收到的是频率累加值,这八个八位再通过位拼接组合成完整的频率累加值;单片机再发送6和7,FPGA接下来两个八位是调幅因子,同样需要进行位拼接操作得到完整值。这里接收到的数据将直接用于dds波形生成部分。这里大家只要知道是这么个功能就行了不需要细纠我的代码。首先是因为这是我自己在SPI的基础上自创的一个协议,大家完全可以自己想到更好的协议;其次是因为这个部分仅适合本工程,其它应用需要做出较大改动。

单片机代码

void arm2fpga()
{
	frequentzi();
	frequentzi2byte();
	vppzi();
	vppzi2byte();
	HAL_SPI_TransmitReceive(&hspi1,&armfpga[0],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&armfpga[1],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&Wave_state,&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&armfpga[2],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&armfpga[3],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&On_state,&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&armfpga[4],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&armfpga[5],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[0],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[1],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[2],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[3],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[4],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[5],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[6],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&freqzibyte[7],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&armfpga[6],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&armfpga[7],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&vzibyte[0],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&vzibyte[1],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
	HAL_SPI_TransmitReceive(&hspi1,&armfpga[8],&RX,sizeof(i),0xFF);
	  printf("%d\r\n",RX);
}

五、遇到的主要难题

1、直接打开spi1的话不会是用和fpga相连的那几个引脚,要用那几个脚的话要自己设置,这是一个值得注意的点,不然会耗费大量时间。

2、Spi通信中寄存器综合时被优化。MISO引脚在行为级仿真表现正常,但是不能通过时序级仿真。

下面是我尝试的解决方法以及结果:

方法1. Synthesis_Options中的-keep hierarchy设置为YES或soft,zhe

在ISE中的综合(XST)选项上右键选择process properties,弹出的对话框里面Synthesis_Options中的-keep hierarchy是设置综合后层次结构的。设置为YES后,用CHIPSCOPE调试时看到的层次结构跟你的设计是一样的,找信号很方便。缺点在于,xilinx 的工具就不能在设计层次间进行设计优化了。所以,建议你设成 “soft”,意思就是综合后保持层次结构,但是P&R的时候可以打破层次结构进行优化。

结果:没有找到radiant内的这个设置,比较了解的同学可以试试看。

方法2. 这种方法简单,但是偶尔寄存器也可能被优化掉

Place the Verilog constraint immediately before the module or instantiation . Specify the Verilog constraint as follows:

       (* KEEP = “{TRUE|FALSE |SOFT}” *)

例如:(*KEEP = "TRUE"*) reg [15:0]  cnt1;//就可以防止cnt1被优化

结果:无效

方法3.把要查看的信号引出到模块的输出端,一般不会被优化掉

结果:有效,将信号引出到输出端后miso工作也变得正常。

这里我列出了试验成功和失败的方法供大家参考,最终是方法3成功了,这也是我会把一些无意义的寄存器设置为输出引脚的原因。

3、在解决后仿真问题之后我的miso依然无法正常工作,耗费大量时间后,发现FPGA需要我按下复位键之后才能开始正常工作。原因是FPGA部分寄存器没有复位处于亚稳态所以不能正常工作。这条就作为经验之谈吧,以后注意。

六、 未来的计划建议

1、对于这个项目指标而言,我认为自己是做到足够好的,希望未来能够精简自己的代码,让代码更加健壮。

2、加一些修饰,UI界面和开机动画还有很大提升空间。

七、附录

1、单片机资源使用情况

FtNhBk7Dmt1FW9AazoC4Ut6J9RkY

2、FPGA资源占用分析

FlXi4f7J-s4ner4ZVS5OIPN5sO4Z

附件下载
DDS_fpga.zip
FPGA部分代码
U8G2_OLED.zip
u8g2库,因内存过大无法与单片机代码一起上传,使用时将其解压到DDS_arm目录下与MDK_ARM同级即可
DDS_arm.zip
单片机部分代码,不全,需要将上面的u8g2库解压到里面
团队介绍
南京邮电大学 朱康
团队成员
zhiwu
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号